diff --git a/services/revolt/revolt.service.js b/services/revolt/revolt.service.js
new file mode 100644
index 0000000000000..e43763e5b722f
--- /dev/null
+++ b/services/revolt/revolt.service.js
@@ -0,0 +1,79 @@
+import Joi from 'joi'
+import { BaseJsonService, pathParam, queryParam } from '../index.js'
+import { nonNegativeInteger, optionalUrl } from '../validators.js'
+
+const schema = Joi.object({
+ member_count: nonNegativeInteger,
+}).required()
+
+const description = `
+The Revolt badge requires an INVITE CODE
to access the Revolt API,
+which can be located at the end of the invitation url.
+
+For example, both
+https://app.revolt.chat/invite/01F7ZSBSFHQ8TA81725KQCSDDP
and
+https://rvlt.gg/01F7ZSBSFHQ8TA81725KQCSDDP
contains an invite code
+of 01F7ZSBSFHQ8TA81725KQCSDDP
.
+`
+
+const queryParamSchema = Joi.object({
+ revolt_api_url: optionalUrl,
+}).required()
+
+export default class RevoltServerInvite extends BaseJsonService {
+ static category = 'chat'
+
+ static route = {
+ base: 'revolt/invite',
+ pattern: ':inviteId',
+ queryParamSchema,
+ }
+
+ static openApi = {
+ '/revolt/invite/{inviteId}': {
+ get: {
+ summary: 'Revolt',
+ description,
+ parameters: [
+ pathParam({
+ name: 'inviteId',
+ example: '01F7ZSBSFHQ8TA81725KQCSDDP',
+ }),
+ queryParam({
+ name: 'revolt_api_url',
+ example: 'https://api.revolt.chat',
+ }),
+ ],
+ },
+ },
+ }
+
+ static defaultBadgeData = { label: 'chat' }
+
+ static render({ memberCount }) {
+ return {
+ message: `${memberCount} members`,
+ color: 'brightgreen',
+ }
+ }
+
+ async fetch({ inviteId, baseUrl }) {
+ return this._requestJson({
+ schema,
+ url: `${baseUrl}/invites/${inviteId}`,
+ })
+ }
+
+ async handle(
+ { inviteId },
+ { revolt_api_url: baseUrl = 'https://api.revolt.chat' },
+ ) {
+ const { member_count: memberCount } = await this.fetch({
+ inviteId,
+ baseUrl,
+ })
+ return this.constructor.render({
+ memberCount,
+ })
+ }
+}
diff --git a/services/revolt/revolt.tester.js b/services/revolt/revolt.tester.js
new file mode 100644
index 0000000000000..aa7a046e957c2
--- /dev/null
+++ b/services/revolt/revolt.tester.js
@@ -0,0 +1,26 @@
+import Joi from 'joi'
+import { createServiceTester } from '../tester.js'
+
+export const t = await createServiceTester()
+
+t.create('get status of #revolt')
+ .get('/01F7ZSBSFHQ8TA81725KQCSDDP.json')
+ .expectBadge({
+ label: 'chat',
+ message: Joi.string().regex(/^[0-9]+ members$/),
+ color: 'brightgreen',
+ })
+
+t.create('custom api url')
+ .get(
+ '/01F7ZSBSFHQ8TA81725KQCSDDP.json?revolt_api_url=https://api.revolt.chat',
+ )
+ .expectBadge({
+ label: 'chat',
+ message: Joi.string().regex(/^[0-9]+ members$/),
+ color: 'brightgreen',
+ })
+
+t.create('invalid invite code')
+ .get('/12345.json')
+ .expectBadge({ label: 'chat', message: 'not found' })