diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..6b2550dc5d6180d126ada837e6dff32965cad4ea
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,40 @@
+.PHONY: update master release setup update_master update_release build clean version
+
+setup:
+	git config --global --add url."git@gitlab.com:".insteadOf "https://gitlab.com/"
+
+version:
+	go run main.go generate
+	mv version_vars.go cmd/version_vars.go
+
+clean:
+	rm -rf vendor/
+	go mod vendor
+
+update:
+	-GOFLAGS="" go get -u all
+
+build:
+	go build ./...
+	go mod tidy
+
+update_release:
+	GOFLAGS="" go get gitlab.com/elixxir/primitives@release
+	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
+	GOFLAGS="" go get gitlab.com/elixxir/crypto@release
+	GOFLAGS="" go get gitlab.com/xx_network/crypto@release
+	GOFLAGS="" go get gitlab.com/elixxir/comms@release
+	GOFLAGS="" go get gitlab.com/xx_network/comms@release
+	GOFLAGS="" go get gitlab.com/elixxir/client@release
+
+update_master:
+	GOFLAGS="" go get gitlab.com/elixxir/primitives@master
+	GOFLAGS="" go get gitlab.com/xx_network/primitives@release
+	GOFLAGS="" go get gitlab.com/elixxir/crypto@master
+	GOFLAGS="" go get gitlab.com/elixxir/comms@master
+	GOFLAGS="" go get gitlab.com/xx_network/comms@master
+	GOFLAGS="" go get gitlab.com/elixxir/client@master
+
+master: update_master clean build version
+
+release: update_release clean build version
diff --git a/cmd/root.go b/cmd/root.go
index ad65315ee8d36c76c9dc5526a6fd5a60ae393a1a..8ee2e7bff2c0eac5a7bf41c6fdec16d3d3e9392a 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"git.xx.network/elixxir/coupons/coupons"
 	"git.xx.network/elixxir/coupons/storage"
+	"github.com/golang/protobuf/proto"
 	"github.com/skip2/go-qrcode"
 	"github.com/spf13/cobra"
 	jww "github.com/spf13/jwalterweatherman"
@@ -28,9 +29,9 @@ var (
 
 // RootCmd represents the base command when called without any sub-commands
 var rootCmd = &cobra.Command{
-	Use:   "UDB",
-	Short: "Runs the cMix UDB server.",
-	Long:  "The cMix UDB server handles user and fact registration for the network.",
+	Use:   "Coins",
+	Short: "Runs the coins bot.",
+	Long:  "The cMix coupon bot handles incoming requests to see coin redemption info",
 	Args:  cobra.NoArgs,
 	Run: func(cmd *cobra.Command, args []string) {
 		// Initialize config & logging
@@ -64,26 +65,61 @@ var rootCmd = &cobra.Command{
 
 		// Get session parameters
 		sessionPath := viper.GetString("sessionPath")
+
+		// Only require proto user path if session does not exist
+		var protoUserJson []byte
+		protoUserPath, err := utils.ExpandPath(viper.GetString("protoUserPath"))
+		if err != nil {
+			jww.FATAL.Fatalf("Failed to read proto path: %+v", err)
+		} else if protoUserPath == "" {
+			jww.WARN.Printf("protoUserPath is blank - a new session will be generated")
+		}
+
 		sessionPass := viper.GetString("sessionPass")
 		networkFollowerTimeout := time.Duration(viper.GetInt("networkFollowerTimeout")) * time.Second
 
-		// Create the client if there's no session
-		if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
-			ndfPath := viper.GetString("ndf")
-			ndfJSON, err := ioutil.ReadFile(ndfPath)
-			if err != nil {
-				jww.FATAL.Panicf("Failed to read NDF: %+v", err)
-			}
+		ndfPath := viper.GetString("ndf")
+		ndfJSON, err := ioutil.ReadFile(ndfPath)
+		if err != nil {
+			jww.FATAL.Panicf("Failed to read NDF: %+v", err)
+		}
+
+		nwParams := params.GetDefaultNetwork()
+
+		useProto := protoUserPath != "" && utils.Exists(protoUserPath)
+		useSession := sessionPath != "" && utils.Exists(sessionPath)
+
+		if !useProto && !useSession {
 			err = api.NewClient(string(ndfJSON), sessionPath, []byte(sessionPass), "")
 			if err != nil {
 				jww.FATAL.Panicf("Failed to create new client: %+v", err)
 			}
+			useSession = true
 		}
 
-		// Create client object
-		cl, err := api.Login(sessionPath, []byte(sessionPass), params.GetDefaultNetwork())
-		if err != nil {
-			jww.FATAL.Panicf("Failed to initialize client: %+v", err)
+		var cl *api.Client
+		if useSession {
+			//  If the session exists, load & login
+			// Create client object
+			cl, err = api.Login(sessionPath, []byte(sessionPass), nwParams)
+			if err != nil {
+				jww.FATAL.Panicf("Failed to initialize client: %+v", err)
+			}
+		} else if useProto {
+			protoUserJson, err = utils.ReadFile(protoUserPath)
+			if err != nil {
+				jww.FATAL.Fatalf("Failed to read proto user at %s: %+v", protoUserPath, err)
+			}
+
+			// If the session does not exist but we have a proto file
+			// Log in using the protofile (attempt to rebuild session)
+			cl, err = api.LoginWithProtoClient(sessionPath,
+				[]byte(sessionPass), protoUserJson, string(ndfJSON), nwParams)
+			if err != nil {
+				jww.FATAL.Fatalf("Failed to create client: %+v", err)
+			}
+		} else {
+			jww.FATAL.Panicf("Cannot run with no session or proto info")
 		}
 
 		// Generate QR code
@@ -91,7 +127,7 @@ var rootCmd = &cobra.Command{
 		qrLevel := qrcode.RecoveryLevel(viper.GetInt("qrLevel"))
 		qrPath := viper.GetString("qrPath")
 		me := cl.GetUser().GetContact()
-		couponsUsername, err := fact.NewFact(fact.Username, "xx Coupons Bot")
+		couponsUsername, err := fact.NewFact(fact.Username, "xx-bonus-coin-bot")
 		if err != nil {
 			jww.FATAL.Panicf("Failed to create username: %+v", err)
 		}
@@ -106,25 +142,62 @@ var rootCmd = &cobra.Command{
 			jww.FATAL.Panicf("Failed to write QR code: %+v", err)
 		}
 
-		// Start network follower
-		err = cl.StartNetworkFollower(networkFollowerTimeout)
-		if err != nil {
-			jww.FATAL.Panicf("Failed to start network follower: %+v", err)
-		}
-
 		// Create & register callback to confirm any authenticated channel requests
-		rcb := func(requestor contact.Contact, message string) {
+		rcb := func(requestor contact.Contact) {
 			rid, err := cl.ConfirmAuthenticatedChannel(requestor)
 			if err != nil {
 				jww.ERROR.Printf("Failed to confirm authenticated channel to %+v: %+v", requestor, err)
 			}
 			jww.DEBUG.Printf("Authenticated channel to %+v created over round %d", requestor, rid)
+
+			time.Sleep(100 * time.Millisecond)
+
+			intro := "Thank you for your interest in xx coin! This bot allows purchasers in the " +
+				"November Community Sale to claim their extra bonus 100% xx coins for using the xx messenger.\n" +
+				"You have received an email from the team with a code you will need to use in order to claim coins.\n" +
+				"Please send that code to this bot, which will confirm your receipt. The coins will be sent to your " +
+				"wallet within 2 weeks.\nCoins will be received in the wallet you received the initial purchase amount in.\n"
+			payload := &coupons.CMIXText{
+				Version: 0,
+				Text:    intro,
+			}
+			marshalled, err := proto.Marshal(payload)
+			if err != nil {
+				jww.ERROR.Printf("Failed to marshal payload: %+v", err)
+				return
+			}
+
+			contact, err := cl.GetAuthenticatedChannelRequest(requestor.ID)
+			if err != nil {
+				jww.ERROR.Printf("Could not get authenticated channel request info: %+v", err)
+				return
+			}
+
+			// Create response message
+			resp := message.Send{
+				Recipient:   contact.ID,
+				Payload:     marshalled,
+				MessageType: message.XxMessage,
+			}
+
+			rids, mid, t, err := cl.SendE2E(resp, params.GetDefaultE2E())
+			if err != nil {
+				jww.ERROR.Printf("Failed to send message: %+v", err)
+				return
+			}
+			jww.INFO.Printf("Sent intro [%+v] to %+v on rounds %+v [%+v]", mid, requestor, rids, t)
 		}
 		cl.GetAuthRegistrar().AddGeneralRequestCallback(rcb)
 
 		// Create coupons impl & register listener on zero user for text messages
 		impl := coupons.New(s, cl)
-		cl.GetSwitchboard().RegisterListener(&id.ZeroUser, message.Text, impl)
+		cl.GetSwitchboard().RegisterListener(&id.ZeroUser, message.XxMessage, impl)
+
+		// Start network follower
+		err = cl.StartNetworkFollower(networkFollowerTimeout)
+		if err != nil {
+			jww.FATAL.Panicf("Failed to start network follower: %+v", err)
+		}
 
 		// Wait 5ever
 		select {}
@@ -141,8 +214,8 @@ func Execute() {
 
 func init() {
 	rootCmd.Flags().StringVarP(&cfgFile, "config", "c", "",
-		"Path to load the UDB configuration file from. If not set, this "+
-			"file must be named udb.yaml and must be located in "+
+		"Path to load the coupons configuration file from. If not set, this "+
+			"file must be named coupons.yaml and must be located in "+
 			"~/.xxnetwork/, /opt/xxnetwork, or /etc/xxnetwork.")
 }
 
@@ -151,7 +224,7 @@ func initConfig() {
 	validConfig = true
 	var err error
 	if cfgFile == "" {
-		cfgFile, err = utils.SearchDefaultLocations("udb.yaml", "xxnetwork")
+		cfgFile, err = utils.SearchDefaultLocations("coupons.yaml", "xxnetwork")
 		if err != nil {
 			validConfig = false
 			jww.FATAL.Panicf("Failed to find config file: %+v", err)
diff --git a/coupons/couponresp.pb.go b/coupons/couponresp.pb.go
index bceaf08046a6f7d4d00d64d6e646a0a6a6715bb4..3e9bab1866b58b9a39418eb98dc1d1472bd1c464 100644
--- a/coupons/couponresp.pb.go
+++ b/coupons/couponresp.pb.go
@@ -21,12 +21,12 @@ var _ = math.Inf
 const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
 
 type CMIXText struct {
-	Text                 string              `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"`
-	Reply                *TextReply          `protobuf:"bytes,2,opt,name=reply,proto3" json:"reply,omitempty"`
-	Preview              *TextNetworkPreview `protobuf:"bytes,3,opt,name=preview,proto3" json:"preview,omitempty"`
-	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
-	XXX_unrecognized     []byte              `json:"-"`
-	XXX_sizecache        int32               `json:"-"`
+	Version              uint32     `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"`
+	Text                 string     `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"`
+	Reply                *TextReply `protobuf:"bytes,3,opt,name=reply,proto3" json:"reply,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
+	XXX_unrecognized     []byte     `json:"-"`
+	XXX_sizecache        int32      `json:"-"`
 }
 
 func (m *CMIXText) Reset()         { *m = CMIXText{} }
@@ -54,6 +54,13 @@ func (m *CMIXText) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_CMIXText proto.InternalMessageInfo
 
+func (m *CMIXText) GetVersion() uint32 {
+	if m != nil {
+		return m.Version
+	}
+	return 0
+}
+
 func (m *CMIXText) GetText() string {
 	if m != nil {
 		return m.Text
@@ -68,83 +75,19 @@ func (m *CMIXText) GetReply() *TextReply {
 	return nil
 }
 
-func (m *CMIXText) GetPreview() *TextNetworkPreview {
-	if m != nil {
-		return m.Preview
-	}
-	return nil
-}
-
-type TextNetworkPreview struct {
-	Url                  string   `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
-	Title                string   `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"`
-	Image                []byte   `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
+type TextReply struct {
+	MessageId            []byte   `protobuf:"bytes,1,opt,name=messageId,proto3" json:"messageId,omitempty"`
+	SenderId             []byte   `protobuf:"bytes,2,opt,name=senderId,proto3" json:"senderId,omitempty"`
 	XXX_NoUnkeyedLiteral struct{} `json:"-"`
 	XXX_unrecognized     []byte   `json:"-"`
 	XXX_sizecache        int32    `json:"-"`
 }
 
-func (m *TextNetworkPreview) Reset()         { *m = TextNetworkPreview{} }
-func (m *TextNetworkPreview) String() string { return proto.CompactTextString(m) }
-func (*TextNetworkPreview) ProtoMessage()    {}
-func (*TextNetworkPreview) Descriptor() ([]byte, []int) {
-	return fileDescriptor_0df6afd1dcee6c11, []int{1}
-}
-
-func (m *TextNetworkPreview) XXX_Unmarshal(b []byte) error {
-	return xxx_messageInfo_TextNetworkPreview.Unmarshal(m, b)
-}
-func (m *TextNetworkPreview) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
-	return xxx_messageInfo_TextNetworkPreview.Marshal(b, m, deterministic)
-}
-func (m *TextNetworkPreview) XXX_Merge(src proto.Message) {
-	xxx_messageInfo_TextNetworkPreview.Merge(m, src)
-}
-func (m *TextNetworkPreview) XXX_Size() int {
-	return xxx_messageInfo_TextNetworkPreview.Size(m)
-}
-func (m *TextNetworkPreview) XXX_DiscardUnknown() {
-	xxx_messageInfo_TextNetworkPreview.DiscardUnknown(m)
-}
-
-var xxx_messageInfo_TextNetworkPreview proto.InternalMessageInfo
-
-func (m *TextNetworkPreview) GetUrl() string {
-	if m != nil {
-		return m.Url
-	}
-	return ""
-}
-
-func (m *TextNetworkPreview) GetTitle() string {
-	if m != nil {
-		return m.Title
-	}
-	return ""
-}
-
-func (m *TextNetworkPreview) GetImage() []byte {
-	if m != nil {
-		return m.Image
-	}
-	return nil
-}
-
-type TextReply struct {
-	Message              string              `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
-	SenderId             []byte              `protobuf:"bytes,2,opt,name=senderId,proto3" json:"senderId,omitempty"`
-	UniqueId             []byte              `protobuf:"bytes,3,opt,name=uniqueId,proto3" json:"uniqueId,omitempty"`
-	Preview              *TextNetworkPreview `protobuf:"bytes,4,opt,name=preview,proto3" json:"preview,omitempty"`
-	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
-	XXX_unrecognized     []byte              `json:"-"`
-	XXX_sizecache        int32               `json:"-"`
-}
-
 func (m *TextReply) Reset()         { *m = TextReply{} }
 func (m *TextReply) String() string { return proto.CompactTextString(m) }
 func (*TextReply) ProtoMessage()    {}
 func (*TextReply) Descriptor() ([]byte, []int) {
-	return fileDescriptor_0df6afd1dcee6c11, []int{2}
+	return fileDescriptor_0df6afd1dcee6c11, []int{1}
 }
 
 func (m *TextReply) XXX_Unmarshal(b []byte) error {
@@ -165,11 +108,11 @@ func (m *TextReply) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_TextReply proto.InternalMessageInfo
 
-func (m *TextReply) GetMessage() string {
+func (m *TextReply) GetMessageId() []byte {
 	if m != nil {
-		return m.Message
+		return m.MessageId
 	}
-	return ""
+	return nil
 }
 
 func (m *TextReply) GetSenderId() []byte {
@@ -179,23 +122,8 @@ func (m *TextReply) GetSenderId() []byte {
 	return nil
 }
 
-func (m *TextReply) GetUniqueId() []byte {
-	if m != nil {
-		return m.UniqueId
-	}
-	return nil
-}
-
-func (m *TextReply) GetPreview() *TextNetworkPreview {
-	if m != nil {
-		return m.Preview
-	}
-	return nil
-}
-
 func init() {
 	proto.RegisterType((*CMIXText)(nil), "coupons.CMIXText")
-	proto.RegisterType((*TextNetworkPreview)(nil), "coupons.TextNetworkPreview")
 	proto.RegisterType((*TextReply)(nil), "coupons.TextReply")
 }
 
@@ -204,21 +132,16 @@ func init() {
 }
 
 var fileDescriptor_0df6afd1dcee6c11 = []byte{
-	// 246 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0xbf, 0x4e, 0xc3, 0x30,
-	0x10, 0xc6, 0xe5, 0xfe, 0x21, 0xcd, 0xd1, 0xa1, 0x3a, 0x31, 0x44, 0xb0, 0x54, 0x99, 0x32, 0x65,
-	0x00, 0xf1, 0x02, 0x30, 0x65, 0x00, 0x55, 0x27, 0x06, 0x56, 0x20, 0x27, 0x64, 0x91, 0xc6, 0xc6,
-	0x7f, 0x68, 0x99, 0x78, 0x09, 0x1e, 0x18, 0xd9, 0x4e, 0x23, 0x10, 0x4b, 0xb7, 0xfb, 0xee, 0x7e,
-	0xf7, 0xf9, 0x3b, 0xc3, 0xea, 0x45, 0x79, 0xad, 0x7a, 0xc3, 0x56, 0xd7, 0xda, 0x28, 0xa7, 0x30,
-	0x4b, 0x1d, 0x5b, 0x7e, 0xc1, 0xe2, 0xf6, 0xae, 0x79, 0x7c, 0xe0, 0xbd, 0x43, 0x84, 0x99, 0xe3,
-	0xbd, 0x2b, 0xc4, 0x5a, 0x54, 0x39, 0xc5, 0x1a, 0x2b, 0x98, 0x1b, 0xd6, 0xdd, 0x67, 0x31, 0x59,
-	0x8b, 0xea, 0xf4, 0x12, 0xeb, 0x61, 0xb1, 0x0e, 0x1b, 0x14, 0x26, 0x94, 0x00, 0xbc, 0x86, 0x4c,
-	0x1b, 0xfe, 0x90, 0xbc, 0x2b, 0xa6, 0x91, 0xbd, 0xf8, 0xc3, 0xde, 0xb3, 0xdb, 0x29, 0xf3, 0xb6,
-	0x49, 0x08, 0x1d, 0xd8, 0x92, 0x00, 0xff, 0x8f, 0x71, 0x05, 0x53, 0x6f, 0xba, 0x21, 0x49, 0x28,
-	0xf1, 0x0c, 0xe6, 0x4e, 0xba, 0x8e, 0x63, 0x90, 0x9c, 0x92, 0x08, 0x5d, 0xb9, 0x7d, 0x7a, 0xe5,
-	0xf8, 0xe4, 0x92, 0x92, 0x28, 0xbf, 0x05, 0xe4, 0x63, 0x3e, 0x2c, 0x20, 0xdb, 0xb2, 0xb5, 0x81,
-	0x4a, 0x7e, 0x07, 0x89, 0xe7, 0xb0, 0xb0, 0xdc, 0xb7, 0x6c, 0x9a, 0x36, 0xda, 0x2e, 0x69, 0xd4,
-	0x61, 0xe6, 0x7b, 0xf9, 0xee, 0xb9, 0x69, 0x07, 0xf3, 0x51, 0xff, 0x3e, 0x75, 0x76, 0xfc, 0xa9,
-	0x37, 0x93, 0x8d, 0x78, 0x3e, 0x89, 0xff, 0x7f, 0xf5, 0x13, 0x00, 0x00, 0xff, 0xff, 0xf3, 0xac,
-	0x09, 0x0f, 0x93, 0x01, 0x00, 0x00,
+	// 173 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x48, 0xce, 0x2f, 0x2d,
+	0xc8, 0xcf, 0x2b, 0x4a, 0x2d, 0x2e, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x87, 0x88,
+	0x14, 0x2b, 0x25, 0x71, 0x71, 0x38, 0xfb, 0x7a, 0x46, 0x84, 0xa4, 0x56, 0x94, 0x08, 0x49, 0x70,
+	0xb1, 0x97, 0xa5, 0x16, 0x15, 0x67, 0xe6, 0xe7, 0x49, 0x30, 0x2a, 0x30, 0x6a, 0xf0, 0x06, 0xc1,
+	0xb8, 0x42, 0x42, 0x5c, 0x2c, 0x25, 0xa9, 0x15, 0x25, 0x12, 0x4c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41,
+	0x60, 0xb6, 0x90, 0x06, 0x17, 0x6b, 0x51, 0x6a, 0x41, 0x4e, 0xa5, 0x04, 0xb3, 0x02, 0xa3, 0x06,
+	0xb7, 0x91, 0x90, 0x1e, 0xd4, 0x48, 0x3d, 0x90, 0x59, 0x41, 0x20, 0x99, 0x20, 0x88, 0x02, 0x25,
+	0x57, 0x2e, 0x4e, 0xb8, 0x98, 0x90, 0x0c, 0x17, 0x67, 0x6e, 0x6a, 0x71, 0x71, 0x62, 0x7a, 0xaa,
+	0x67, 0x0a, 0xd8, 0x1a, 0x9e, 0x20, 0x84, 0x80, 0x90, 0x14, 0x17, 0x47, 0x71, 0x6a, 0x5e, 0x4a,
+	0x6a, 0x91, 0x67, 0x0a, 0xd8, 0x32, 0x9e, 0x20, 0x38, 0x3f, 0x89, 0x0d, 0xec, 0x74, 0x63, 0x40,
+	0x00, 0x00, 0x00, 0xff, 0xff, 0x96, 0x78, 0xf4, 0x8f, 0xce, 0x00, 0x00, 0x00,
 }
diff --git a/coupons/couponresp.proto b/coupons/couponresp.proto
index 5437690084a5a310c02fc6ccdea105eb7a17b094..a28fe1ca95f429fc33f5d5d58cedb1775aaf9700 100644
--- a/coupons/couponresp.proto
+++ b/coupons/couponresp.proto
@@ -2,24 +2,14 @@ syntax = "proto3";
 
 package coupons;
 
-// message, enum should be define in package level
-option java_multiple_files = true;
-
 message CMIXText {
-    string text = 1;
-    TextReply reply = 2;
-    TextNetworkPreview preview = 3;
-}
-
-message TextNetworkPreview {
-    string url = 1;
-    string title = 2;
-    bytes image = 3;
+    uint32 version = 1;
+    string text = 2;
+    TextReply reply = 3;
 }
 
 message TextReply {
-    string message = 1;
+    bytes messageId = 1;
     bytes senderId = 2;
-    bytes uniqueId = 3;
-    TextNetworkPreview preview = 4;
 }
+
diff --git a/coupons/listener.go b/coupons/listener.go
index c710ec53fabb1f01f6e80f9dee768557b258af65..c66d5d9f2fefef1e141b0a834758a679f14ed5b5 100644
--- a/coupons/listener.go
+++ b/coupons/listener.go
@@ -1,63 +1,128 @@
 package coupons
 
 import (
+	"errors"
+	"fmt"
 	"git.xx.network/elixxir/coupons/storage"
 	"github.com/golang/protobuf/proto"
 	jww "github.com/spf13/jwalterweatherman"
 	"gitlab.com/elixxir/client/api"
 	"gitlab.com/elixxir/client/interfaces/message"
 	"gitlab.com/elixxir/client/interfaces/params"
+	"gorm.io/gorm"
+	"strconv"
+	"strings"
+	"time"
 )
 
 type listener struct {
-	s *storage.Storage
-	c *api.Client
+	delay time.Duration
+	s     *storage.Storage
+	c     *api.Client
 }
 
+var validResponse = "Thank you! You will receive %d xx in %s within 2 weeks!"
+var noWalletFound = "Thank you! You will receive %d xx. The xx team does not have a wallet address for your purchase, please contact %s."
+var reusedCode = "Your code %s has already been used. Your wallet %s will receive %d xx within 2 weeks."
+var invalidCode = "That is not a valid code. Please send the code you received in your email for participating in the November Community Sale. Please contact %s if you need support."
+var accountUsed = "Your messenger account can only redeem once."
+var supportEmail = "distribution@xx-coin.io"
+
 // Hear messages from users to the coupon bot & respond appropriately
 func (l *listener) Hear(item message.Receive) {
 	// Confirm that authenticated channels
 	if !l.c.HasAuthenticatedChannel(item.Sender) {
 		jww.ERROR.Printf("No authenticated channel exists to %+v", item.Sender)
+		return
 	}
 
 	// Parse the trigger
 	in := &CMIXText{}
-	trigger := ""
+	var trigger string
 	err := proto.Unmarshal(item.Payload, in)
 	if err != nil {
-		jww.ERROR.Printf("Could not unmartial message from messenger: %+v", err)
+		jww.ERROR.Printf("Could not unmarshal message from messenger: %+v", err)
+		return
 	} else {
 		trigger = in.Text
 	}
 
+	jww.INFO.Printf("Received trigger %s [%+v]", trigger, in)
+	var strResponse string
+
+	// Check if the user is in the db, if it is return the trigger
+	usedTrigger, err := l.s.CheckUser(item.Sender.String())
+	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // Exit if we get error other than record not found
+		jww.DEBUG.Printf("Failed to check user with DB: %+v", err)
+		return
+	}
+
 	// Retrieve coupon code for trigger if it exists
-	strResponse, err := l.s.GetCouponCode(trigger)
+	codeResp, uses, err := l.s.GetCouponCode(trigger)
 	if err != nil {
 		jww.DEBUG.Printf("No coupon code for trigger %s: %+v", trigger, err)
-		strResponse = "No coupon found for trigger " + trigger
+		strResponse = fmt.Sprintf(invalidCode, supportEmail)
+	} else {
+		// Split the data in code column
+		rl := strings.Split(codeResp, ",")
+		// Assign wallet to var
+		wallet := rl[1]
+		// Parse num coins
+		num, err := strconv.Atoi(rl[0])
+		if err != nil {
+			jww.ERROR.Printf("Could not parse num coins: %+v", err)
+			return
+		}
+		jww.DEBUG.Printf("Found %s with %d uses", codeResp, uses)
+
+		// Response logic
+		if usedTrigger != "" && usedTrigger != trigger { // If account has a code used that isn't the one passed in
+			jww.INFO.Println("Case 1")
+			strResponse = accountUsed
+		} else if uses < 1 { // If the code has been used
+			jww.INFO.Println("Case 2")
+			strResponse = fmt.Sprintf(reusedCode, trigger, wallet, num)
+		} else { // Code is good and can be used by this account
+			jww.INFO.Println("Case 3")
+			err = l.s.UseCode(item.Sender.String(), trigger)
+			if err != nil {
+				jww.ERROR.Printf("Failed to commit code use to db: %+v", err)
+			}
+			if wallet == "" { //
+				strResponse = fmt.Sprintf(noWalletFound, num, supportEmail)
+			} else {
+				strResponse = fmt.Sprintf(validResponse, num, wallet)
+			}
+		}
 	}
 
 	payload := &CMIXText{
-		Text: strResponse,
+		Version: 0,
+		Text:    strResponse,
+		Reply: &TextReply{
+			MessageId: item.ID.Marshal(),
+			SenderId:  item.Sender.Marshal(),
+		},
 	}
 	marshalled, err := proto.Marshal(payload)
 	if err != nil {
 		jww.ERROR.Printf("Failed to marshal payload: %+v", err)
+		return
 	}
 	// Create response message
 	resp := message.Send{
 		Recipient:   item.Sender,
 		Payload:     marshalled,
-		MessageType: message.Text,
+		MessageType: message.XxMessage,
 	}
 
 	// Send response message to sender over cmix
 	rids, mid, t, err := l.c.SendE2E(resp, params.GetDefaultE2E())
 	if err != nil {
 		jww.ERROR.Printf("Failed to send message: %+v", err)
+	} else {
+		jww.INFO.Printf("Sent response %s [%+v] to %+v on rounds %+v [%+v]", strResponse, mid, item.Sender.String(), rids, t)
 	}
-	jww.INFO.Printf("Sent response %s [%+v] to %+v on rounds %+v [%+v]", strResponse, mid, item.Sender.String(), rids, t)
 }
 
 // Name returns a name, used for debugging
diff --git a/go.mod b/go.mod
index fe20eaeffc70b71d726816e6f00ffeb38e7397ee..a57385d33c9a6bef32ee9b537501a7e04444517c 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,10 @@ require (
 	github.com/spf13/cobra v1.2.1
 	github.com/spf13/jwalterweatherman v1.1.0
 	github.com/spf13/viper v1.9.0
-	gitlab.com/elixxir/client v1.5.1-0.20211108203412-047b81be6c67
-	gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c
-	gitlab.com/elixxir/primitives v0.0.3-0.20211014164029-06022665b576
-	gitlab.com/xx_network/primitives v0.0.4-0.20211014163031-53405cf191fb
+	gitlab.com/elixxir/client v1.5.1-0.20220202192535-930327652941
+	gitlab.com/elixxir/crypto v0.0.7-0.20220110170041-7e42f2e8b062
+	gitlab.com/elixxir/primitives v0.0.3-0.20220104173924-275cb9d7834f
+	gitlab.com/xx_network/primitives v0.0.4-0.20211222205802-03e9d7d835b0
 	gorm.io/driver/postgres v1.1.2
 	gorm.io/gorm v1.21.15
 )
diff --git a/go.sum b/go.sum
index 94f82c9bbce1dfbadd5e0dab2960198322b01e56..52d55028e08157eecea5bc94681e918e39006f53 100644
--- a/go.sum
+++ b/go.sum
@@ -62,16 +62,24 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
+github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.0.1-0.20211008185751-59b49bc148ce h1:2s+cfEmFVdtV8Z85o6U0QxtNhCXDCMR2OLZKgL39ApI=
+github.com/cloudflare/circl v1.0.1-0.20211008185751-59b49bc148ce/go.mod h1:tnEeRn/onb0b4Ew40H00boTlcVMHveaTzi6m+/iMruw=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
 github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
@@ -89,6 +97,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A=
+github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -96,6 +106,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
 github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
@@ -351,6 +362,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -460,34 +473,34 @@ github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l
 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 h1:Gi6rj4mAlK0BJIk1HIzBVMjWNjIUfstrsXC2VqLYPcA=
 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k=
-gitlab.com/elixxir/client v1.5.1-0.20211108203412-047b81be6c67 h1:0eR95gzo2L5fJlWkRmasXmGb9155/Pr26GAoFEUzQI0=
-gitlab.com/elixxir/client v1.5.1-0.20211108203412-047b81be6c67/go.mod h1:YYGrpEby2kGV26hzfRciqyOvTSZBtMiGi8QLg4jMymo=
-gitlab.com/elixxir/comms v0.0.4-0.20211029171408-dcd58978e446 h1:wmrdEN8H+nCLkZ09S41OyYI/Nj/9Gg6ra6p8iuJ6pjg=
-gitlab.com/elixxir/comms v0.0.4-0.20211029171408-dcd58978e446/go.mod h1:rQpTeFVSn08ocbQeEw5AbMhGWXHfXmQ0y1/ZprAIVVU=
+gitlab.com/elixxir/client v1.5.1-0.20220202192535-930327652941 h1:YbVSJyx1T7nD5KY/cykpKE+sDctzWT2yS88GEfrEEXE=
+gitlab.com/elixxir/client v1.5.1-0.20220202192535-930327652941/go.mod h1:Ji6FOGgiVckBV2vaWd5Qh3feoYiMFvro/4PZuPVPILg=
+gitlab.com/elixxir/comms v0.0.4-0.20220128193157-34178165415d h1:207Okb8+amKnRzsKE/4ehl3eb6ZHkWXevq9TimOehzw=
+gitlab.com/elixxir/comms v0.0.4-0.20220128193157-34178165415d/go.mod h1:pj1TXrpHKytF68y53BtCBRYhaiMJWquuVBk4iEN7wkk=
 gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c=
 gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA=
-gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c h1:HIr2HBhZqSAKdPRBdEY0/qravISL619O2yuTY/DQTdo=
-gitlab.com/elixxir/crypto v0.0.7-0.20211022013957-3a7899285c4c/go.mod h1:teuTEXyqsqo4N/J1sshcTg9xYOt+wNTurop7pkZOiCg=
-gitlab.com/elixxir/ekv v0.1.5 h1:R8M1PA5zRU1HVnTyrtwybdABh7gUJSCvt1JZwUSeTzk=
-gitlab.com/elixxir/ekv v0.1.5/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4=
+gitlab.com/elixxir/crypto v0.0.7-0.20220110170041-7e42f2e8b062 h1:6LLdEX2U/jA3RakJh/cKtjckMYBBjrjyowoBEtIF9L4=
+gitlab.com/elixxir/crypto v0.0.7-0.20220110170041-7e42f2e8b062/go.mod h1:qmW0OGPB21GcaGg1Jvt527/qUw7ke6W8DKCiYBfsx48=
+gitlab.com/elixxir/ekv v0.1.6 h1:M2hUSNhH/ChxDd+s8xBqSEKgoPtmE6hOEBqQ73KbN6A=
+gitlab.com/elixxir/ekv v0.1.6/go.mod h1:e6WPUt97taFZe5PFLPb1Dupk7tqmDCTQu1kkstqJvw4=
 gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg=
 gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0VelQda72OzoUckr1O+vPW0AiFe0nyKQ6gYcmFSuF8=
 gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc=
 gitlab.com/elixxir/primitives v0.0.1/go.mod h1:kNp47yPqja2lHSiS4DddTvFpB/4D9dB2YKnw5c+LJCE=
-gitlab.com/elixxir/primitives v0.0.3-0.20211014164029-06022665b576 h1:sXX3/hewV4TQLxT2iKBfnfgW/A1eXoEfv5raJxTb79s=
-gitlab.com/elixxir/primitives v0.0.3-0.20211014164029-06022665b576/go.mod h1:zZy8AlOISFm5IG4G4sylypnz7xNBfZ5mpXiibqJT8+8=
+gitlab.com/elixxir/primitives v0.0.3-0.20220104173924-275cb9d7834f h1:zg3oYk+a6Wmq9tGRwka3GjJR1XRZIVCsOMpBGxtF2yc=
+gitlab.com/elixxir/primitives v0.0.3-0.20220104173924-275cb9d7834f/go.mod h1:zA+1Lp9fGPo6pl1QxtMoNPLeZJ1O5m4kcH7HNxePQnQ=
 gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw=
-gitlab.com/xx_network/comms v0.0.4-0.20211014163953-e774276b83ae h1:jmZWmSm8eH40SX5B5uOw2XaYoHYqVn8daTfa6B80AOs=
-gitlab.com/xx_network/comms v0.0.4-0.20211014163953-e774276b83ae/go.mod h1:wR9Vx0KZLrIs0g2Efcp0UwFPStjcDRWkg/DJLVQI2vw=
+gitlab.com/xx_network/comms v0.0.4-0.20220126231737-fe2338016cce h1:PCOf9nyehdz2IIUsssakxH+YT2q3YQ6OlAFpcOgsd88=
+gitlab.com/xx_network/comms v0.0.4-0.20220126231737-fe2338016cce/go.mod h1:5arueRMa2MNa6dALnfJwyZOhqhV53Gqc+tlHRz+Ycjw=
 gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE=
 gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk=
-gitlab.com/xx_network/crypto v0.0.5-0.20211014163843-57b345890686 h1:mEjKISxi9LrguYgz6evroFwsfxH78/hYmr32yws+WV0=
-gitlab.com/xx_network/crypto v0.0.5-0.20211014163843-57b345890686/go.mod h1:GeUUB5eMlu7G1u7LXpClfOyUYsSDxAhiZBf+RZeGftc=
+gitlab.com/xx_network/crypto v0.0.5-0.20211227194420-f311e8920467 h1:LkZtWBYrM2e7QRf5aaBAcy7s7CpYGhAqgXRFVCdBRy4=
+gitlab.com/xx_network/crypto v0.0.5-0.20211227194420-f311e8920467/go.mod h1:c+x0w3Xk6QZe5w2Redn5SiaBpqAhgNSfwBr0JGa/yyo=
 gitlab.com/xx_network/primitives v0.0.0-20200803231956-9b192c57ea7c/go.mod h1:wtdCMr7DPePz9qwctNoAUzZtbOSHSedcK++3Df3psjA=
 gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da/go.mod h1:OK9xevzWCaPO7b1wiluVJGk7R5ZsuC7pHY5hteZFQug=
 gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc=
-gitlab.com/xx_network/primitives v0.0.4-0.20211014163031-53405cf191fb h1:0K9dyxFpDYzH9jYLwzg3+bRj9a0uJjwjQkMeIdTxduQ=
-gitlab.com/xx_network/primitives v0.0.4-0.20211014163031-53405cf191fb/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
+gitlab.com/xx_network/primitives v0.0.4-0.20211222205802-03e9d7d835b0 h1:IHHb59DJEKk02HgfxddqK2ilvkRMAUdPIBFn8rmjjIg=
+gitlab.com/xx_network/primitives v0.0.4-0.20211222205802-03e9d7d835b0/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE=
 gitlab.com/xx_network/ring v0.0.3-0.20210527191221-ce3f170aabd5 h1:FY+4Rh1Q2rgLyv10aKJjhWApuKRCR/054XhreudfAvw=
 gitlab.com/xx_network/ring v0.0.3-0.20210527191221-ce3f170aabd5/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -707,8 +720,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
 golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE=
+golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -906,8 +920,9 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
 google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
 google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
 google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
 google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
+google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
diff --git a/storage/database.go b/storage/database.go
index d8c2c9b10ecbe7a55580eaa99e3567cd62bb8d82..eb7ec0cd3d3920153def64ad6a962977fb2fcc06 100644
--- a/storage/database.go
+++ b/storage/database.go
@@ -13,8 +13,10 @@ import (
 
 // database interface holds function definitions for storage
 type database interface {
-	GetCouponCode(trigger string) (string, error)
+	CheckUser(id string) (string, error)
+	GetCouponCode(trigger string) (string, int, error)
 	InsertCoupon(c Coupon) error
+	UseCode(id, trigger string) error
 }
 
 // DatabaseImpl struct implements the database interface with an underlying DB
@@ -24,15 +26,22 @@ type DatabaseImpl struct {
 
 // MapImpl struct implements the database interface with an underlying Map
 type MapImpl struct {
-	coupons map[string]Coupon
+	coupons map[string]*Coupon
+	users   map[string]*Coupon
 	sync.RWMutex
 }
 
+type User struct {
+	ID      string `gorm:"primary_key"`
+	Trigger string
+	Coupon  Coupon `gorm:"foreignkey:trigger;references:trigger"`
+}
+
 // Coupon struct defines coupons in the db
 type Coupon struct {
 	Trigger string `gorm:"primary_key"`
-	Code    string `gorm:"NOT NULL;unique;"`
-	Uses    uint
+	Code    string `gorm:"NOT NULL;"`
+	Uses    int
 }
 
 // newDatabase initializes the database interface
@@ -70,7 +79,8 @@ func newDatabase(username, password, dbName, address,
 		defer jww.INFO.Println("Map backend initialized successfully!")
 
 		mapImpl := &MapImpl{
-			coupons: map[string]Coupon{},
+			coupons: map[string]*Coupon{},
+			users:   map[string]*Coupon{},
 		}
 
 		return database(mapImpl), nil
@@ -92,7 +102,7 @@ func newDatabase(username, password, dbName, address,
 
 	// Initialize the database schema
 	// WARNING: Order is important. Do not change without database testing
-	models := []interface{}{Coupon{}}
+	models := []interface{}{Coupon{}, User{}}
 	for _, model := range models {
 		err = db.AutoMigrate(model)
 		if err != nil {
diff --git a/storage/database_test.go b/storage/database_test.go
index de55cf774fe1bcd2b929a52da7c97e1d13344023..d9a6b9812b0ea8920d0a15984f7ce50ce4c09ad7 100644
--- a/storage/database_test.go
+++ b/storage/database_test.go
@@ -13,8 +13,9 @@ func TestDatabase(t *testing.T) {
 	if err != nil {
 		t.Errorf("Failed to initialize storage: %+v", err)
 	}
+	trigger := "up up down down left right left right b a start"
 	err = s.InsertCoupon(Coupon{
-		Trigger: "up up down down left right left right b a start",
+		Trigger: trigger,
 		Code:    "helpimtrappedinacouponfactory",
 		Uses:    1,
 	})
@@ -22,7 +23,7 @@ func TestDatabase(t *testing.T) {
 		t.Errorf("Failed to insert coupon: %+v", err)
 	}
 
-	c, err := s.GetCouponCode("up up down down left right left right b a start")
+	c, _, err := s.GetCouponCode("up up down down left right left right b a start")
 	if err != nil {
 		t.Errorf("Failed to get coupon code: %+v", err)
 	}
@@ -31,8 +32,13 @@ func TestDatabase(t *testing.T) {
 		t.Errorf("Did not get expected coupon")
 	}
 
-	_, err = s.GetCouponCode("up up down down left right left right b a start")
-	if err == nil {
-		t.Error("Should have given error w/ no uses left")
+	err = s.UseCode("zezima", trigger)
+	if err != nil {
+		t.Errorf("Failed to use code: %+v", err)
+	}
+
+	_, uses, err := s.GetCouponCode("up up down down left right left right b a start")
+	if uses > 0 {
+		t.Error("Should not have any uses left")
 	}
 }
diff --git a/storage/dbImpl.go b/storage/dbImpl.go
index 928c75759cb216bf57e58f36411c4cac99f5131d..0d1e2807b5c6a38a4e1f4fbb644b8e1115f4a593 100644
--- a/storage/dbImpl.go
+++ b/storage/dbImpl.go
@@ -1,34 +1,56 @@
 package storage
 
 import (
-	"errors"
+	"github.com/pkg/errors"
 	"gorm.io/gorm"
 )
 
-func (db *DatabaseImpl) GetCouponCode(trigger string) (string, error) {
+func (db *DatabaseImpl) GetCouponCode(trigger string) (string, int, error) {
 	var c = &Coupon{}
 	err := db.db.Transaction(func(tx *gorm.DB) error {
 		err := tx.First(&c, "trigger = ?", trigger).Error
 		if err != nil {
 			return err
 		}
-		if c.Uses <= 0 {
-			return errors.New("requested code is out of uses")
-		}
-		err = tx.Model(&c).Where("trigger = ?", trigger).
-			Update("uses", gorm.Expr("uses - ?", 1)).Error
-		if err != nil {
-			return err
-		}
 		return nil
 	})
 	if err != nil {
-		return "", err
+		return "", 0, err
 	}
 
-	return c.Code, nil
+	return c.Code, int(c.Uses), nil
 }
 
 func (db *DatabaseImpl) InsertCoupon(c Coupon) error {
 	return db.db.Create(c).Error
 }
+
+func (db *DatabaseImpl) CheckUser(id string) (string, error) {
+	u := &User{}
+	err := db.db.Where("id = ?", id).Take(&u).Error
+	if err != nil {
+		return "", err
+	}
+	return u.Trigger, nil
+}
+
+func (db *DatabaseImpl) UseCode(id, trigger string) error {
+	return db.db.Transaction(func(tx *gorm.DB) error {
+		u := &User{
+			ID:      id,
+			Trigger: trigger,
+		}
+		err := tx.Create(&u).Error
+		if err != nil {
+			return errors.WithMessage(err, "Failed to add user")
+		}
+
+		c := &Coupon{}
+		err = tx.Model(&c).Where("trigger = ?", trigger).
+			Update("uses", gorm.Expr("uses - ?", 1)).Error
+		if err != nil {
+			return errors.WithMessage(err, "Failed to use code")
+		}
+		return nil
+	})
+}
diff --git a/storage/mapImpl.go b/storage/mapImpl.go
index 24489826072ced38f1f9e27fafe61c96a4c43723..667f892a6268f1ef1c27b666d91e8068eb00a911 100644
--- a/storage/mapImpl.go
+++ b/storage/mapImpl.go
@@ -3,22 +3,39 @@ package storage
 import (
 	"errors"
 	"fmt"
+	"gorm.io/gorm"
 )
 
-func (m *MapImpl) GetCouponCode(trigger string) (string, error) {
+func (m *MapImpl) GetCouponCode(trigger string) (string, int, error) {
 	c, ok := m.coupons[trigger]
 	if !ok {
-		return "", errors.New(fmt.Sprintf("No coupon for trigger %s", trigger))
+		return "", 0, errors.New(fmt.Sprintf("No coupon for trigger %s", trigger))
 	}
+	uses := int(c.Uses)
 	if c.Uses <= 0 {
-		return "", errors.New("No uses left for requested coupon")
+		return "", uses, errors.New("No uses left for requested coupon")
 	}
-	c.Uses = c.Uses - 1
 	m.coupons[trigger] = c
-	return c.Code, nil
+	return c.Code, uses, nil
 }
 
 func (m *MapImpl) InsertCoupon(c Coupon) error {
-	m.coupons[c.Trigger] = c
+	m.coupons[c.Trigger] = &c
+	return nil
+}
+
+func (m *MapImpl) CheckUser(id string) (string, error) {
+	u, ok := m.users[id]
+	if !ok {
+		return "", gorm.ErrRecordNotFound
+	}
+	return u.Trigger, nil
+}
+
+func (m *MapImpl) UseCode(id, trigger string) error {
+	m.users[id] = m.coupons[trigger]
+	c := m.coupons[trigger]
+	fmt.Println(m.coupons)
+	c.Uses = c.Uses - 1
 	return nil
 }