diff --git a/main.go b/main.go
index 0449c007dd28ebc2d8517954c419d04f6483f417..517987bd1d0b959f61edf22b18a01f2b32063f05 100644
--- a/main.go
+++ b/main.go
@@ -33,6 +33,7 @@ func main() {
 	http.HandleFunc("/api/release/", s.handleReleaseMirror)
 	http.HandleFunc("/api/apps/zip/", s.handleAppZip)
 	http.HandleFunc("/api/apps/tar/", s.handleAppTargz)
+	http.HandleFunc("/api/apps/shame.json", s.handleAppShame)
 	http.HandleFunc("/api/apps/", s.handleApp)
 	log.Printf("Listening on %s...", flagListen)
 	http.ListenAndServe(flagListen, nil)
diff --git a/server.go b/server.go
index 85cfd4931d1bce090ab3afbab38d79eef9715eb0..a46601a7cffc0ecf78eb43bb84c742ab636eb3b3 100644
--- a/server.go
+++ b/server.go
@@ -22,7 +22,7 @@ type server struct {
 
 type req struct {
 	getReleases    chan []GLRelease
-	getAppRegistry chan []*appDescriptor
+	getAppRegistry chan *appRegistry
 }
 
 func (s *server) run(ctx context.Context) {
@@ -70,11 +70,22 @@ func (s *server) getReleases(ctx context.Context) ([]GLRelease, error) {
 }
 
 func (s *server) getApps(ctx context.Context) ([]*appDescriptor, error) {
-	sresC := make(chan []*appDescriptor)
+	sresC := make(chan *appRegistry)
 	select {
 	case s.reqC <- &req{getAppRegistry: sresC}:
 		res := <-sresC
-		return res, nil
+		return res.apps, nil
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	}
+}
+
+func (s *server) getAppShame(ctx context.Context) ([]*appShame, error) {
+	sresC := make(chan *appRegistry)
+	select {
+	case s.reqC <- &req{getAppRegistry: sresC}:
+		res := <-sresC
+		return res.shame, nil
 	case <-ctx.Done():
 		return nil, ctx.Err()
 	}
diff --git a/server_apps.go b/server_apps.go
index c5ede95057dfeae507088bddeb9ef4d8f3daaaeb..7c324c77a681d7565fbe6df8dd7318e8f7d74b1a 100644
--- a/server_apps.go
+++ b/server_apps.go
@@ -26,6 +26,16 @@ import (
 	"github.com/pelletier/go-toml/v2"
 )
 
+type appRegistry struct {
+	shame []*appShame
+	apps  []*appDescriptor
+}
+
+type appShame struct {
+	repository string
+	errorMsg   string
+}
+
 type appDescriptor struct {
 	repository string
 	pathInRepo string
@@ -386,7 +396,7 @@ func (s *server) getAppInfo(ctx context.Context, pathInRepo, repo string) (*appI
 	return app, nil
 }
 
-func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
+func (s *server) getAppRegistry(ctx context.Context) (*appRegistry, error) {
 	s.gitMu.Lock()
 	defer s.gitMu.Unlock()
 	if s.appRepo == nil {
@@ -421,7 +431,7 @@ func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
 		return nil, err
 	}
 
-	var res []*appDescriptor
+	var registry appRegistry
 	for {
 		f, err := iter.Next()
 		if err == io.EOF {
@@ -440,6 +450,10 @@ func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
 		reader, err := f.Reader()
 		if err != nil {
 			log.Printf("App %q: couldn't read TOML: %v", m[1], err)
+			registry.shame = append(registry.shame, &appShame{
+				repository: fmt.Sprintf("<app-list toml: %q>", m[1]),
+				errorMsg:   fmt.Sprintf("couldn't read TOML: %v", err),
+			})
 			continue
 		}
 
@@ -452,23 +466,35 @@ func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
 		reader.Close()
 		if err != nil {
 			log.Printf("App %q: couldn't parse TOML: %v", m[1], err)
+			registry.shame = append(registry.shame, &appShame{
+				repository: fmt.Sprintf("<app-list toml: %q>", m[1]),
+				errorMsg:   fmt.Sprintf("couldn't read TOML: %v", err),
+			})
 			continue
 		}
 		n := reAppRepo.FindStringSubmatch(dat.Repo)
 		if n == nil {
 			log.Printf("App %q: invalid repo %q", m[1], dat.Repo)
+			registry.shame = append(registry.shame, &appShame{
+				repository: fmt.Sprintf("<app-list toml: %q>", m[1]),
+				errorMsg:   fmt.Sprintf("invalid repo %q", dat.Repo),
+			})
 			continue
 		}
-		res = append(res, &appDescriptor{
+		registry.apps = append(registry.apps, &appDescriptor{
 			repository: dat.Repo,
 			pathInRepo: dat.PathInRepo,
 		})
 	}
 
-	for _, app := range res {
+	for _, app := range registry.apps {
 		info, err := s.getAppInfo(ctx, app.pathInRepo, app.repository)
 		if err != nil {
 			log.Printf("App %q: %v", app.repository, err)
+			registry.shame = append(registry.shame, &appShame{
+				repository: app.repository,
+				errorMsg:   fmt.Sprintf("%v", err),
+			})
 			continue
 		} else {
 			log.Printf("App: %q: okay", app.repository)
@@ -476,8 +502,9 @@ func (s *server) getAppRegistry(ctx context.Context) ([]*appDescriptor, error) {
 		app.appInfo = info
 	}
 
-	log.Printf("Got %d apps", len(res))
-	return res, nil
+	log.Printf("Got %d apps and %d apps with shame", len(registry.apps), len(registry.shame))
+
+	return &registry, nil
 }
 
 type jsonApp struct {
@@ -573,6 +600,39 @@ func (s *server) handleApp(w http.ResponseWriter, r *http.Request) {
 	http.NotFound(w, r)
 }
 
+func (s *server) handleAppShame(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	appShame, err := s.getAppShame(ctx)
+	if err != nil {
+		return
+	}
+
+	type jsonAppShame struct {
+		Repo     string `json:"repo"`
+		ErrorMsg string `json:"errorMsg"`
+	}
+
+	type res struct {
+		DT   time.Time      `json:"isodatetime"`
+		Apps []jsonAppShame `json:"shame"`
+	}
+
+	resp := res{
+		DT: time.Now(),
+	}
+	for _, shame := range appShame {
+		// Guarenteed to be exactly 2 parts because of the regex
+		resp.Apps = append(resp.Apps, jsonAppShame{
+			Repo:     shame.repository,
+			ErrorMsg: shame.errorMsg,
+		})
+	}
+	w.Header().Add("Access-Control-Allow-Origin", "*")
+	w.Header().Add("Content-Type", "application/json")
+	j := json.NewEncoder(w)
+	j.Encode(resp)
+}
+
 var (
 	reAppZipURL   = regexp.MustCompile(`^/api/apps/zip/([^/]+)/([^/]+)\.zip$`)
 	reAppTargzURL = regexp.MustCompile(`^/api/apps/tar/([^/]+)/([^/]+)\.tar\.gz$`)