diff --git a/services/polymart/polymart-base.js b/services/polymart/polymart-base.js
new file mode 100644
index 0000000000000000000000000000000000000000..9eec32824368c1a9294608bdaa5e5c6db4bb246d
--- /dev/null
+++ b/services/polymart/polymart-base.js
@@ -0,0 +1,51 @@
+import Joi from 'joi'
+import { BaseJsonService } from '../index.js'
+
+const resourceSchema = Joi.object({
+  response: Joi.object({
+    resource: Joi.object({
+      price: Joi.number().required(),
+      downloads: Joi.string().required(),
+      reviews: Joi.object({
+        count: Joi.number().required(),
+        stars: Joi.number().required(),
+      }).required(),
+      updates: Joi.object({
+        latest: Joi.object({
+          version: Joi.string().required(),
+        }).required(),
+      }).required(),
+    }).required(),
+  }).required(),
+}).required()
+
+const notFoundResourceSchema = Joi.object({
+  response: Joi.object({
+    success: Joi.boolean().required(),
+    errors: Joi.object().required(),
+  }).required(),
+})
+
+const resourceFoundOrNotSchema = Joi.alternatives(
+  resourceSchema,
+  notFoundResourceSchema
+)
+
+const documentation = `
+<p>You can find your resource ID in the url for your resource page.</p>
+<p>Example: <code>https://polymart.org/resource/polymart-plugin.323</code> - Here the Resource ID is 323.</p>`
+
+class BasePolymartService extends BaseJsonService {
+  async fetch({
+    resourceId,
+    schema = resourceFoundOrNotSchema,
+    url = `https://api.polymart.org/v1/getResourceInfo/?resource_id=${resourceId}`,
+  }) {
+    return this._requestJson({
+      schema,
+      url,
+    })
+  }
+}
+
+export { documentation, BasePolymartService }
diff --git a/services/polymart/polymart-downloads.service.js b/services/polymart/polymart-downloads.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe2d4f641ccea0c20a85d8a9aa12a9616418f6fc
--- /dev/null
+++ b/services/polymart/polymart-downloads.service.js
@@ -0,0 +1,35 @@
+import { NotFound } from '../../core/base-service/errors.js'
+import { renderDownloadsBadge } from '../downloads.js'
+import { BasePolymartService, documentation } from './polymart-base.js'
+
+export default class PolymartDownloads extends BasePolymartService {
+  static category = 'downloads'
+
+  static route = {
+    base: 'polymart/downloads',
+    pattern: ':resourceId',
+  }
+
+  static examples = [
+    {
+      title: 'Polymart Downloads',
+      namedParams: {
+        resourceId: '323',
+      },
+      staticPreview: renderDownloadsBadge({ downloads: 655 }),
+      documentation,
+    },
+  ]
+
+  static defaultBadgeData = {
+    label: 'downloads',
+  }
+
+  async handle({ resourceId }) {
+    const { response } = await this.fetch({ resourceId })
+    if (!response.resource) {
+      throw new NotFound()
+    }
+    return renderDownloadsBadge({ downloads: response.resource.downloads })
+  }
+}
diff --git a/services/polymart/polymart-downloads.tester.js b/services/polymart/polymart-downloads.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..4bc7f3c35664698078fe99a93fe3d6295d7859b8
--- /dev/null
+++ b/services/polymart/polymart-downloads.tester.js
@@ -0,0 +1,13 @@
+import { isMetric } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('Polymart Plugin (id 323)').get('/323.json').expectBadge({
+  label: 'downloads',
+  message: isMetric,
+})
+
+t.create('Invalid Resource (id 0)').get('/0.json').expectBadge({
+  label: 'downloads',
+  message: 'not found',
+})
diff --git a/services/polymart/polymart-latest-version.service.js b/services/polymart/polymart-latest-version.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..aa6083374e04960955c05507ec5a438c45534645
--- /dev/null
+++ b/services/polymart/polymart-latest-version.service.js
@@ -0,0 +1,38 @@
+import { NotFound } from '../../core/base-service/errors.js'
+import { renderVersionBadge } from '../version.js'
+import { BasePolymartService, documentation } from './polymart-base.js'
+export default class PolymartLatestVersion extends BasePolymartService {
+  static category = 'version'
+
+  static route = {
+    base: 'polymart/version',
+    pattern: ':resourceId',
+  }
+
+  static examples = [
+    {
+      title: 'Polymart Version',
+      namedParams: {
+        resourceId: '323',
+      },
+      staticPreview: renderVersionBadge({
+        version: 'v1.2.9',
+      }),
+      documentation,
+    },
+  ]
+
+  static defaultBadgeData = {
+    label: 'polymart',
+  }
+
+  async handle({ resourceId }) {
+    const { response } = await this.fetch({ resourceId })
+    if (!response.resource) {
+      throw new NotFound()
+    }
+    return renderVersionBadge({
+      version: response.resource.updates.latest.version,
+    })
+  }
+}
diff --git a/services/polymart/polymart-latest-version.tester.js b/services/polymart/polymart-latest-version.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..bff2a8e3c37f68da3181abebcab98a67cea01634
--- /dev/null
+++ b/services/polymart/polymart-latest-version.tester.js
@@ -0,0 +1,13 @@
+import { isVPlusDottedVersionNClauses } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('Polymart Plugin (id 323)').get('/323.json').expectBadge({
+  label: 'polymart',
+  message: isVPlusDottedVersionNClauses,
+})
+
+t.create('Invalid Resource (id 0)').get('/0.json').expectBadge({
+  label: 'polymart',
+  message: 'not found',
+})
diff --git a/services/polymart/polymart-rating.service.js b/services/polymart/polymart-rating.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa067c5861d60309ad8db2ec91053de0ef028b4b
--- /dev/null
+++ b/services/polymart/polymart-rating.service.js
@@ -0,0 +1,65 @@
+import { starRating, metric } from '../text-formatters.js'
+import { floorCount } from '../color-formatters.js'
+import { NotFound } from '../../core/base-service/errors.js'
+import { BasePolymartService, documentation } from './polymart-base.js'
+
+export default class PolymartRatings extends BasePolymartService {
+  static category = 'rating'
+
+  static route = {
+    base: 'polymart',
+    pattern: ':format(rating|stars)/:resourceId',
+  }
+
+  static examples = [
+    {
+      title: 'Polymart Stars',
+      pattern: 'stars/:resourceId',
+      namedParams: {
+        resourceId: '323',
+      },
+      staticPreview: this.render({
+        format: 'stars',
+        total: 14,
+        average: 5,
+      }),
+      documentation,
+    },
+    {
+      title: 'Polymart Rating',
+      pattern: 'rating/:resourceId',
+      namedParams: {
+        resourceId: '323',
+      },
+      staticPreview: this.render({ total: 14, average: 5 }),
+      documentation,
+    },
+  ]
+
+  static defaultBadgeData = {
+    label: 'rating',
+  }
+
+  static render({ format, total, average }) {
+    const message =
+      format === 'stars'
+        ? starRating(average)
+        : `${average}/5 (${metric(total)})`
+    return {
+      message,
+      color: floorCount(average, 2, 3, 4),
+    }
+  }
+
+  async handle({ format, resourceId }) {
+    const { response } = await this.fetch({ resourceId })
+    if (!response.resource) {
+      throw new NotFound()
+    }
+    return this.constructor.render({
+      format,
+      total: response.resource.reviews.count,
+      average: response.resource.reviews.stars.toFixed(2),
+    })
+  }
+}
diff --git a/services/polymart/polymart-rating.tester.js b/services/polymart/polymart-rating.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..547d0d988f6c380c8ff92fd490ca110b27c86b3b
--- /dev/null
+++ b/services/polymart/polymart-rating.tester.js
@@ -0,0 +1,27 @@
+import { isStarRating, withRegex } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('Stars - Polymart Plugin (id 323)')
+  .get('/stars/323.json')
+  .expectBadge({
+    label: 'rating',
+    message: isStarRating,
+  })
+
+t.create('Stars - Invalid Resource (id 0)').get('/stars/0.json').expectBadge({
+  label: 'rating',
+  message: 'not found',
+})
+
+t.create('Rating - Polymart Plugin (id 323)')
+  .get('/rating/323.json')
+  .expectBadge({
+    label: 'rating',
+    message: withRegex(/^(\d*\.\d+)(\/5 \()(\d+)(\))$/),
+  })
+
+t.create('Rating - Invalid Resource (id 0)').get('/rating/0.json').expectBadge({
+  label: 'rating',
+  message: 'not found',
+})