diff --git a/bitmask/darwin.go b/bitmask/darwin.go
index f3e92247c27761d37e859c3bbc3ac11a60d090f5..84934360c881a818b3f9c7a5b981d05afa2d236e 100644
--- a/bitmask/darwin.go
+++ b/bitmask/darwin.go
@@ -4,4 +4,4 @@ package bitmask
 
 import "os"
 
-var configPath = os.Getenv("HOME") + "/Library/Preferences/leap"
+var ConfigPath = os.Getenv("HOME") + "/Library/Preferences/leap"
diff --git a/bitmask/events.go b/bitmask/events.go
index 8645519b154ba9cd2a5e75840fd485d169232ea2..5808d51bc9c1a4d18a2c0e53191abca9df225bd0 100644
--- a/bitmask/events.go
+++ b/bitmask/events.go
@@ -72,5 +72,5 @@ func (b *Bitmask) eventsHandler() {
 }
 
 func getServerKeyPath() string {
-	return filepath.Join(configPath, "events", "zmq_certificates", "public_keys", "server.key")
+	return filepath.Join(ConfigPath, "events", "zmq_certificates", "public_keys", "server.key")
 }
diff --git a/bitmask/unix.go b/bitmask/unix.go
index 5b50f24c13d0cacb327b5639134b6de9897498a3..5bb522c1b4c45822df667e114cbcc998e9ac3850 100644
--- a/bitmask/unix.go
+++ b/bitmask/unix.go
@@ -4,4 +4,4 @@ package bitmask
 
 import "os"
 
-var configPath = os.Getenv("HOME") + "/.config/leap"
+var ConfigPath = os.Getenv("HOME") + "/.config/leap"
diff --git a/bitmask/windows.go b/bitmask/windows.go
index a574c9667841430119575d8230a1ec308aa3dc10..36481f5d029baa9c141346fce76ad0375a150a2e 100644
--- a/bitmask/windows.go
+++ b/bitmask/windows.go
@@ -4,4 +4,4 @@ package bitmask
 
 import "os"
 
-var configPath = os.Getenv("APPDATA") + "\\leap"
+var ConfigPath = os.Getenv("APPDATA") + "\\leap"
diff --git a/config.go b/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..0a769aa15c77f7db5a2bdabdb8bc21188be0f7f4
--- /dev/null
+++ b/config.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+	"path"
+	"time"
+
+	"0xacab.org/leap/bitmask-systray/bitmask"
+)
+
+const (
+	oneDay   = time.Hour * 24
+	oneMonth = oneDay * 30
+)
+
+var (
+	configPath = path.Join(bitmask.ConfigPath, "systray.json")
+)
+
+type systrayConfig struct {
+	LastNotification time.Time
+	Donated          time.Time
+}
+
+func parseConfig() (*systrayConfig, error) {
+	var conf systrayConfig
+
+	f, err := os.Open(configPath)
+	if os.IsNotExist(err) {
+		return &conf, nil
+	}
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+
+	dec := json.NewDecoder(f)
+	err = dec.Decode(&conf)
+	return &conf, err
+}
+
+func (c *systrayConfig) hasDonated() bool {
+	log.Println("has donated ", c.Donated.Add(oneMonth))
+	return c.Donated.Add(oneMonth).After(time.Now())
+}
+
+func (c *systrayConfig) needsNotification() bool {
+	log.Println("needs ", c.LastNotification.Add(oneDay))
+	log.Println(!c.hasDonated() && c.LastNotification.Add(oneDay).Before(time.Now()))
+	return !c.hasDonated() && c.LastNotification.Add(oneDay).Before(time.Now())
+}
+
+func (c *systrayConfig) setNotification() error {
+	c.LastNotification = time.Now()
+	return c.save()
+}
+
+func (c *systrayConfig) setDonated() error {
+	c.Donated = time.Now()
+	return c.save()
+}
+
+func (c *systrayConfig) save() error {
+	f, err := os.Create(configPath)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	enc := json.NewEncoder(f)
+	return enc.Encode(c)
+}
diff --git a/main.go b/main.go
index ce06c97c26e7ff24f4a71ea046f0c9b21a34c237..76343ebb1d353f26fb02e89cdb23ed57261c87a9 100644
--- a/main.go
+++ b/main.go
@@ -11,12 +11,18 @@ const (
 )
 
 func main() {
-	go notificate()
+	// TODO: do I need to bootstrap the provider?
+	conf, err := parseConfig()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	go notificate(conf)
 
 	b, err := bitmask.Init()
 	if err != nil {
 		log.Fatal(err)
 	}
 
-	run(b)
+	run(b, conf)
 }
diff --git a/notificator.go b/notificator.go
index 1e6022185582688f1156b3cc9a32be97c59bba75..2a46b001f7d6728cdf0e76de7eadd03f2f2a53b9 100644
--- a/notificator.go
+++ b/notificator.go
@@ -8,18 +8,23 @@ import (
 	"github.com/0xAX/notificator"
 )
 
-const notificationText = `The RiseupVPN service is expensive to run. Because we don't want to store personal information about you, there is no accounts or billing for this service. But if you want the service to continue, donate at least $5 each month at https://riseup.net/donate-vpn`
+const (
+	notificationText = `The RiseupVPN service is expensive to run. Because we don't want to store personal information about you, there is no accounts or billing for this service. But if you want the service to continue, donate at least $5 each month at https://riseup.net/donate-vpn`
+)
 
-func notificate() {
+func notificate(conf *systrayConfig) {
 	wd, _ := os.Getwd()
 	notify := notificator.New(notificator.Options{
 		DefaultIcon: path.Join(wd, "riseupvpn.svg"),
 		AppName:     "RiseupVPN",
 	})
 
+	time.Sleep(time.Minute * 5)
 	for {
-		time.Sleep(time.Minute * 5)
-		notify.Push("Donate to RiseupVPN", notificationText, "", notificator.UR_NORMAL)
-		time.Sleep(time.Hour * 24)
+		if conf.needsNotification() {
+			notify.Push("Donate to RiseupVPN", notificationText, "", notificator.UR_NORMAL)
+			conf.setNotification()
+		}
+		time.Sleep(time.Hour)
 	}
 }
diff --git a/systray.go b/systray.go
index 3da4d19a021db5d9e996da0cc852eead29773d01..4344748ef1ba59bc8a81e45ae328a5aab333f266 100644
--- a/systray.go
+++ b/systray.go
@@ -13,10 +13,13 @@ import (
 
 type bmTray struct {
 	bm            *bitmask.Bitmask
+	conf          *systrayConfig
 	waitCh        chan bool
 	mStatus       *systray.MenuItem
 	mTurnOn       *systray.MenuItem
 	mTurnOff      *systray.MenuItem
+	mDonate       *systray.MenuItem
+	mHaveDonated  *systray.MenuItem
 	mCancel       *systray.MenuItem
 	activeGateway *gatewayTray
 }
@@ -26,8 +29,8 @@ type gatewayTray struct {
 	name     string
 }
 
-func run(bm *bitmask.Bitmask) {
-	bt := bmTray{bm: bm}
+func run(bm *bitmask.Bitmask, conf *systrayConfig) {
+	bt := bmTray{bm: bm, conf: conf}
 	systray.Run(bt.onReady, bt.onExit)
 }
 
@@ -50,7 +53,8 @@ func (bt *bmTray) onReady() {
 	bt.addGateways()
 
 	mHelp := systray.AddMenuItem("Help ...", "")
-	mDonate := systray.AddMenuItem("Donate ...", "")
+	bt.mDonate = systray.AddMenuItem("Donate ...", "")
+	bt.mHaveDonated = systray.AddMenuItem("... I have donated", "")
 	mAbout := systray.AddMenuItem("About ...", "")
 	systray.AddSeparator()
 
@@ -66,6 +70,8 @@ func (bt *bmTray) onReady() {
 		}
 
 		for {
+			bt.updateDonateMenu()
+
 			select {
 			case status := <-ch:
 				log.Println("status: " + status)
@@ -83,8 +89,10 @@ func (bt *bmTray) onReady() {
 
 			case <-mHelp.ClickedCh:
 				open.Run("https://riseup.net/vpn")
-			case <-mDonate.ClickedCh:
+			case <-bt.mDonate.ClickedCh:
 				open.Run("https://riseup.net/donate-vpn")
+			case <-bt.mHaveDonated.ClickedCh:
+				bt.conf.setDonated()
 			case <-mAbout.ClickedCh:
 				open.Run("https://bitmask.net")
 
@@ -93,6 +101,8 @@ func (bt *bmTray) onReady() {
 				bt.bm.Close()
 				log.Println("Quit now...")
 				os.Exit(0)
+
+			case <-time.After(time.Minute * 30):
 			}
 		}
 	}()
@@ -192,6 +202,16 @@ func (bt *bmTray) changeStatus(status string) {
 	bt.mStatus.SetTooltip("RiseupVPN is " + statusStr)
 }
 
+func (bt *bmTray) updateDonateMenu() {
+	if bt.conf.hasDonated() {
+		go bt.mHaveDonated.Hide()
+		go bt.mDonate.Hide()
+	} else {
+		go bt.mHaveDonated.Show()
+		go bt.mDonate.Show()
+	}
+}
+
 func (bt *bmTray) waitIcon() {
 	icons := [][]byte{icon.Wait0, icon.Wait1, icon.Wait2, icon.Wait3}
 	for i := 0; true; i = (i + 1) % 4 {