diff --git a/services/npm-stat/npm-stat-downloads.service.js b/services/npm-stat/npm-stat-downloads.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..99e4a28bdd137837222e0e6a021d0e68a48400bf
--- /dev/null
+++ b/services/npm-stat/npm-stat-downloads.service.js
@@ -0,0 +1,71 @@
+import Joi from 'joi'
+import dayjs from 'dayjs'
+import { nonNegativeInteger } from '../validators.js'
+import { BaseJsonService } from '../index.js'
+import { renderDownloadsBadge } from '../downloads.js'
+
+const schema = Joi.object()
+  .pattern(Joi.string(), Joi.object().pattern(Joi.string(), nonNegativeInteger))
+  .required()
+
+const intervalMap = {
+  dw: { interval: 'week' },
+  dm: { interval: 'month' },
+  dy: { interval: 'year' },
+}
+
+export default class NpmStatDownloads extends BaseJsonService {
+  static category = 'downloads'
+
+  static route = {
+    base: 'npm-stat',
+    pattern: ':interval(dw|dm|dy)/:author',
+  }
+
+  static examples = [
+    {
+      title: 'npm (by author)',
+      documentation:
+        'The total number of downloads of npm packages published by the specified author from [npm-stat](https://npm-stat.com).',
+      namedParams: { interval: 'dy', author: 'dukeluo' },
+      staticPreview: this.render({ interval: 'dy', downloadCount: 30000 }),
+      keywords: ['node'],
+    },
+  ]
+
+  static _cacheLength = 21600
+
+  static defaultBadgeData = { label: 'downloads' }
+
+  static getTotalDownloads(data) {
+    const add = (x, y) => x + y
+    const sum = nums => nums.reduce(add, 0)
+
+    return Object.values(data).reduce(
+      (count, packageDownloads) => count + sum(Object.values(packageDownloads)),
+      0,
+    )
+  }
+
+  static render({ interval, downloads }) {
+    return renderDownloadsBadge({
+      downloads,
+      interval: intervalMap[interval].interval,
+      colorOverride: downloads > 0 ? 'brightgreen' : 'red',
+    })
+  }
+
+  async handle({ interval, author }) {
+    const unit = intervalMap[interval].interval
+    const today = dayjs()
+    const until = today.format('YYYY-MM-DD')
+    const from = today.subtract(1, unit).format('YYYY-MM-DD')
+    const data = await this._requestJson({
+      url: `https://npm-stat.com/api/download-counts?author=${author}&from=${from}&until=${until}`,
+      schema,
+    })
+    const downloads = this.constructor.getTotalDownloads(data)
+
+    return this.constructor.render({ interval, downloads })
+  }
+}
diff --git a/services/npm-stat/npm-stat-downloads.spec.js b/services/npm-stat/npm-stat-downloads.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1932b9b650a7a9af3caac9dd196ad73aa01097fe
--- /dev/null
+++ b/services/npm-stat/npm-stat-downloads.spec.js
@@ -0,0 +1,25 @@
+import { test, given } from 'sazerac'
+import NpmStatDownloads from './npm-stat-downloads.service.js'
+
+describe('NpmStatDownloads helpers', function () {
+  test(NpmStatDownloads.getTotalDownloads, () => {
+    given({
+      'hexo-theme-candelas': {
+        '2022-12-01': 1,
+        '2022-12-02': 2,
+        '2022-12-03': 3,
+      },
+      '@dukeluo/fanjs': {
+        '2022-12-01': 10,
+        '2022-12-02': 20,
+        '2022-12-03': 30,
+      },
+      'eslint-plugin-check-file': {
+        '2022-12-01': 100,
+        '2022-12-02': 200,
+        '2022-12-03': 300,
+      },
+    }).expect(666)
+    given({}).expect(0)
+  })
+})
diff --git a/services/npm-stat/npm-stat-downloads.tester.js b/services/npm-stat/npm-stat-downloads.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..830a4208f204cfb6de6af844674a6b1a8c8971e3
--- /dev/null
+++ b/services/npm-stat/npm-stat-downloads.tester.js
@@ -0,0 +1,35 @@
+import { isMetricOverTimePeriod } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('weekly downloads of npm author dukeluo')
+  .get('/dw/dukeluo.json')
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+    color: 'brightgreen',
+  })
+
+t.create('monthly downloads of npm author dukeluo')
+  .get('/dm/dukeluo.json')
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+    color: 'brightgreen',
+  })
+
+t.create('yearly downloads of npm author dukeluo')
+  .get('/dy/dukeluo.json')
+  .expectBadge({
+    label: 'downloads',
+    message: isMetricOverTimePeriod,
+    color: 'brightgreen',
+  })
+
+t.create('downloads of unknown npm package author')
+  .get('/dy/npm-api-does-not-have-this-package-author.json')
+  .expectBadge({
+    label: 'downloads',
+    message: '0/year',
+    color: 'red',
+  })