diff --git a/services/greasyfork/greasyfork-base.js b/services/greasyfork/greasyfork-base.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9c474647c4a274c6f07e07541aba88e0bb28453
--- /dev/null
+++ b/services/greasyfork/greasyfork-base.js
@@ -0,0 +1,24 @@
+import Joi from 'joi'
+import { nonNegativeInteger } from '../validators.js'
+import { BaseJsonService } from '../index.js'
+
+const schema = Joi.object({
+  daily_installs: nonNegativeInteger,
+  total_installs: nonNegativeInteger,
+  good_ratings: nonNegativeInteger,
+  ok_ratings: nonNegativeInteger,
+  bad_ratings: nonNegativeInteger,
+  version: Joi.string().required(),
+  license: Joi.string().allow(null).required(),
+}).required()
+
+export default class BaseGreasyForkService extends BaseJsonService {
+  static defaultBadgeData = { label: 'greasy fork' }
+
+  async fetch({ scriptId }) {
+    return this._requestJson({
+      schema,
+      url: `https://greasyfork.org/scripts/${scriptId}.json`,
+    })
+  }
+}
diff --git a/services/greasyfork/greasyfork-downloads.service.js b/services/greasyfork/greasyfork-downloads.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..cc6f6ce3adf25d210bd8324a87402913f617a22a
--- /dev/null
+++ b/services/greasyfork/greasyfork-downloads.service.js
@@ -0,0 +1,35 @@
+import { renderDownloadsBadge } from '../downloads.js'
+import BaseGreasyForkService from './greasyfork-base.js'
+
+export default class GreasyForkInstalls extends BaseGreasyForkService {
+  static category = 'downloads'
+  static route = { base: 'greasyfork', pattern: ':variant(dt|dd)/:scriptId' }
+
+  static examples = [
+    {
+      title: 'Greasy Fork',
+      pattern: 'dd/:scriptId',
+      namedParams: { scriptId: '407466' },
+      staticPreview: renderDownloadsBadge({ downloads: 17 }),
+    },
+    {
+      title: 'Greasy Fork',
+      pattern: 'dt/:scriptId',
+      namedParams: { scriptId: '407466' },
+      staticPreview: renderDownloadsBadge({ downloads: 3420 }),
+    },
+  ]
+
+  static defaultBadgeData = { label: 'installs' }
+
+  async handle({ variant, scriptId }) {
+    const data = await this.fetch({ scriptId })
+    if (variant === 'dd') {
+      const downloads = data.daily_installs
+      const interval = 'day'
+      return renderDownloadsBadge({ downloads, interval })
+    }
+    const downloads = data.total_installs
+    return renderDownloadsBadge({ downloads })
+  }
+}
diff --git a/services/greasyfork/greasyfork-downloads.tester.js b/services/greasyfork/greasyfork-downloads.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc69e7c1cbaf38629a800b2efed51f53d97d38ae
--- /dev/null
+++ b/services/greasyfork/greasyfork-downloads.tester.js
@@ -0,0 +1,19 @@
+import { createServiceTester } from '../tester.js'
+import { isMetric, isMetricOverTimePeriod } from '../test-validators.js'
+export const t = await createServiceTester()
+
+t.create('Daily Installs')
+  .get('/dd/407466.json')
+  .expectBadge({ label: 'installs', message: isMetricOverTimePeriod })
+
+t.create('Daily Installs (not found)')
+  .get('/dd/000000.json')
+  .expectBadge({ label: 'installs', message: 'not found' })
+
+t.create('Total Installs')
+  .get('/dt/407466.json')
+  .expectBadge({ label: 'installs', message: isMetric })
+
+t.create('Total Installs (not found)')
+  .get('/dt/000000.json')
+  .expectBadge({ label: 'installs', message: 'not found' })
diff --git a/services/greasyfork/greasyfork-license.service.js b/services/greasyfork/greasyfork-license.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..895b3a97a6c77d8fb494cd5e017daa9efd1ec521
--- /dev/null
+++ b/services/greasyfork/greasyfork-license.service.js
@@ -0,0 +1,34 @@
+import { renderLicenseBadge } from '../licenses.js'
+import { InvalidResponse } from '../index.js'
+import BaseGreasyForkService from './greasyfork-base.js'
+
+export default class GreasyForkLicense extends BaseGreasyForkService {
+  static category = 'license'
+  static route = { base: 'greasyfork', pattern: 'l/:scriptId' }
+
+  static examples = [
+    {
+      title: 'Greasy Fork',
+      namedParams: { scriptId: '407466' },
+      staticPreview: renderLicenseBadge({ licenses: ['MIT'] }),
+    },
+  ]
+
+  static defaultBadgeData = { label: 'license' }
+
+  transform({ data }) {
+    if (data.license === null) {
+      throw new InvalidResponse({
+        prettyMessage: 'license not found',
+      })
+    }
+    // remove suffix " License" from data.license
+    return { license: data.license.replace(/ License$/, '') }
+  }
+
+  async handle({ scriptId }) {
+    const data = await this.fetch({ scriptId })
+    const { license } = this.transform({ data })
+    return renderLicenseBadge({ licenses: [license] })
+  }
+}
diff --git a/services/greasyfork/greasyfork-license.tester.js b/services/greasyfork/greasyfork-license.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..bf410b73e18c10070ae580ed80cd0040dbea2e06
--- /dev/null
+++ b/services/greasyfork/greasyfork-license.tester.js
@@ -0,0 +1,11 @@
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('License (valid)').get('/l/407466.json').expectBadge({
+  label: 'license',
+  message: 'MIT',
+})
+
+t.create('License (not found)')
+  .get('/l/000000.json')
+  .expectBadge({ label: 'license', message: 'not found' })
diff --git a/services/greasyfork/greasyfork-version.service.js b/services/greasyfork/greasyfork-version.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..9759703d6a3577acac98d1438669fdcf21e8ae7d
--- /dev/null
+++ b/services/greasyfork/greasyfork-version.service.js
@@ -0,0 +1,20 @@
+import { renderVersionBadge } from '../version.js'
+import BaseGreasyForkService from './greasyfork-base.js'
+
+export default class GreasyForkVersion extends BaseGreasyForkService {
+  static category = 'version'
+  static route = { base: 'greasyfork', pattern: 'v/:scriptId' }
+
+  static examples = [
+    {
+      title: 'Greasy Fork',
+      namedParams: { scriptId: '407466' },
+      staticPreview: renderVersionBadge({ version: '3.9.3' }),
+    },
+  ]
+
+  async handle({ scriptId }) {
+    const data = await this.fetch({ scriptId })
+    return renderVersionBadge({ version: data.version })
+  }
+}
diff --git a/services/greasyfork/greasyfork-version.tester.js b/services/greasyfork/greasyfork-version.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..d4c2877dd47367720107cc5a720396f4d9dbd05e
--- /dev/null
+++ b/services/greasyfork/greasyfork-version.tester.js
@@ -0,0 +1,12 @@
+import { isVPlusDottedVersionAtLeastOne } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('Version').get('/v/407466.json').expectBadge({
+  label: 'greasy fork',
+  message: isVPlusDottedVersionAtLeastOne,
+})
+
+t.create('Version (not found)')
+  .get('/v/000000.json')
+  .expectBadge({ label: 'greasy fork', message: 'not found' })