···192192 r.Use(h.requireAuth)
193193 r.Post(atproto.HoldRequestCrew, h.HandleRequestCrew)
194194 })
195195+196196+ // Public quota endpoint (no auth - quota is per-user, just needs userDid param)
197197+ r.Get(atproto.HoldGetQuota, h.HandleGetQuota)
195198}
196199197200// HandleHealth returns health check information
···15131516 // Clients should use multipart upload flow via com.atproto.repo.uploadBlob
15141517 return ""
15151518}
15191519+15201520+// HandleGetQuota returns storage quota information for a user
15211521+// This calculates the total unique blob storage used by a specific user
15221522+// by iterating layer records and deduplicating by digest.
15231523+func (h *XRPCHandler) HandleGetQuota(w http.ResponseWriter, r *http.Request) {
15241524+ userDID := r.URL.Query().Get("userDid")
15251525+ if userDID == "" {
15261526+ http.Error(w, "missing required parameter: userDid", http.StatusBadRequest)
15271527+ return
15281528+ }
15291529+15301530+ // Validate DID format
15311531+ if !atproto.IsDID(userDID) {
15321532+ http.Error(w, "invalid userDid format", http.StatusBadRequest)
15331533+ return
15341534+ }
15351535+15361536+ // Get quota stats
15371537+ stats, err := h.pds.GetQuotaForUser(r.Context(), userDID)
15381538+ if err != nil {
15391539+ slog.Error("Failed to get quota", "userDid", userDID, "error", err)
15401540+ http.Error(w, fmt.Sprintf("failed to get quota: %v", err), http.StatusInternalServerError)
15411541+ return
15421542+ }
15431543+15441544+ w.Header().Set("Content-Type", "application/json")
15451545+ json.NewEncoder(w).Encode(stats)
15461546+}