diff --git a/.github/actions/service-tests/action.yml b/.github/actions/service-tests/action.yml
index 480ed7abb07cd8dd0d9c2c047b29e58178c154e5..ca10f539cb0041a839baa72e794c3e463b8fc299 100644
--- a/.github/actions/service-tests/action.yml
+++ b/.github/actions/service-tests/action.yml
@@ -36,10 +36,6 @@ inputs:
     description: 'The SERVICETESTS_TWITCH_CLIENT_SECRET secret'
     required: false
     default: ''
-  wheelmap-token:
-    description: 'The SERVICETESTS_WHEELMAP_TOKEN secret'
-    required: false
-    default: ''
   youtube-api-key:
     description: 'The SERVICETESTS_YOUTUBE_API_KEY secret'
     required: false
@@ -75,7 +71,6 @@ runs:
         SL_INSIGHT_API_TOKEN: '${{ inputs.sl-insight-api-token }}'
         TWITCH_CLIENT_ID: '${{ inputs.twitch-client-id }}'
         TWITCH_CLIENT_SECRET: '${{ inputs.twitch-client-secret }}'
-        WHEELMAP_TOKEN: '${{ inputs.wheelmap-token }}'
         YOUTUBE_API_KEY: '${{ inputs.youtube-api-key }}'
 
     - name: Write Markdown Summary
diff --git a/.github/workflows/coveralls-code-coverage.yml b/.github/workflows/coveralls-code-coverage.yml
index 235e8b8ea674b2e9a9f1c46ff9d2f0c2bf50aa72..99a78376de9c03367c29821a2f06505694cb6015 100644
--- a/.github/workflows/coveralls-code-coverage.yml
+++ b/.github/workflows/coveralls-code-coverage.yml
@@ -64,7 +64,6 @@ jobs:
           SL_INSIGHT_API_TOKEN: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
           TWITCH_CLIENT_ID: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
           TWITCH_CLIENT_SECRET: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
-          WHEELMAP_TOKEN: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
           YOUTUBE_API_KEY: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
         shell: bash
 
diff --git a/.github/workflows/daily-tests.yml b/.github/workflows/daily-tests.yml
index 6145b8b78eb2687e565e5f25cac687850522a326..995fbb1e5deafb958bb33caefb6e8a7d73f817dc 100644
--- a/.github/workflows/daily-tests.yml
+++ b/.github/workflows/daily-tests.yml
@@ -61,7 +61,6 @@ jobs:
           SL_INSIGHT_API_TOKEN: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
           TWITCH_CLIENT_ID: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
           TWITCH_CLIENT_SECRET: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
-          WHEELMAP_TOKEN: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
           YOUTUBE_API_KEY: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
 
       - name: Write Service Tests Markdown Summary
diff --git a/.github/workflows/deploy-review-app.yml b/.github/workflows/deploy-review-app.yml
index f4c69c9a4016c1769c38abf67471ea6a3867a254..8b2a4bfb3e28a4b1627c70752f000cc41a251573 100644
--- a/.github/workflows/deploy-review-app.yml
+++ b/.github/workflows/deploy-review-app.yml
@@ -40,5 +40,4 @@ jobs:
             SL_INSIGHT_USER_UUID=${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}
             TWITCH_CLIENT_ID=${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}
             TWITCH_CLIENT_SECRET=${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}
-            WHEELMAP_TOKEN=${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}
             YOUTUBE_API_KEY=${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}
diff --git a/.github/workflows/test-services-22.yml b/.github/workflows/test-services-22.yml
index cc6fd43867ee358198372f8be144cba6a9159bad..bcbe3e09d3b45736c09c4dc4372d2d516343d4a8 100644
--- a/.github/workflows/test-services-22.yml
+++ b/.github/workflows/test-services-22.yml
@@ -34,7 +34,6 @@ jobs:
           sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
           twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
           twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
-          wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
           youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
 
       - name: Service tests (triggered from fork)
diff --git a/.github/workflows/test-services.yml b/.github/workflows/test-services.yml
index d8c9a4d2c146f8dd9d7d1a005242688b0a859b53..6fbf9bcba4a1235cb5d34dc7589b5c901e37f741 100644
--- a/.github/workflows/test-services.yml
+++ b/.github/workflows/test-services.yml
@@ -32,7 +32,6 @@ jobs:
           sl-insight-api-token: '${{ secrets.SERVICETESTS_SL_INSIGHT_API_TOKEN }}'
           twitch-client-id: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_ID }}'
           twitch-client-secret: '${{ secrets.SERVICETESTS_TWITCH_CLIENT_SECRET }}'
-          wheelmap-token: '${{ secrets.SERVICETESTS_WHEELMAP_TOKEN }}'
           youtube-api-key: '${{ secrets.SERVICETESTS_YOUTUBE_API_KEY }}'
 
       - name: Service tests (triggered from fork)
diff --git a/app.json b/app.json
deleted file mode 100644
index 646f5adb61f65c5eff5a7299bf5297738e8f3aaf..0000000000000000000000000000000000000000
--- a/app.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
-  "name": "Shields",
-  "description": "Concise, consistent, and legible badges in SVG and raster format.",
-  "keywords": ["badge", "github", "svg", "status"],
-  "website": "https://shields.io/",
-  "repository": "https://github.com/badges/shields",
-  "logo": "https://shields.io/favicon.png",
-  "env": {
-    "CYPRESS_INSTALL_BINARY": {
-      "description": "Disable the cypress binary installation",
-      "value": "0",
-      "required": false
-    },
-    "HUSKY_SKIP_INSTALL": {
-      "description": "Skip the husky git hook setup",
-      "value": "1",
-      "required": false
-    },
-    "WHEELMAP_TOKEN": {
-      "description": "Configure the token to be used for the Wheelmap service.",
-      "required": false
-    },
-    "GH_TOKEN": {
-      "description": "Configure the token to be used for the GitHub services.",
-      "required": false
-    },
-    "TWITCH_CLIENT_ID": {
-      "description": "Configure the client id to be used for the Twitch service.",
-      "required": false
-    },
-    "TWITCH_CLIENT_SECRET": {
-      "description": "Configure the client secret to be used for the Twitch service.",
-      "required": false
-    },
-    "WEBLATE_API_KEY": {
-      "description": "Configure the API key to be used for the Weblate service.",
-      "required": false
-    },
-    "METRICS_INFLUX_ENABLED": {
-      "description": "Disable influx metrics",
-      "value": "false",
-      "required": false
-    },
-    "REQUIRE_CLOUDFLARE": {
-      "description": "Allow direct traffic",
-      "value": "false",
-      "required": false
-    }
-  },
-  "formation": {
-    "web": {
-      "quantity": 1,
-      "size": "free"
-    }
-  }
-}
diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml
index 6686b245c8faa904355160a46c090c3fdc34cb75..bbe982eadff338dd5001e9cd9ada2625dac68b77 100644
--- a/config/custom-environment-variables.yml
+++ b/config/custom-environment-variables.yml
@@ -114,7 +114,6 @@ private:
   teamcity_pass: 'TEAMCITY_PASS'
   twitch_client_id: 'TWITCH_CLIENT_ID'
   twitch_client_secret: 'TWITCH_CLIENT_SECRET'
-  wheelmap_token: 'WHEELMAP_TOKEN'
   influx_username: 'INFLUX_USERNAME'
   influx_password: 'INFLUX_PASSWORD'
   weblate_api_key: 'WEBLATE_API_KEY'
diff --git a/core/server/server.js b/core/server/server.js
index f5621d94112578335cec1a12e3a562bc50e425df..e50ae7392b8d97842334b988361075fb8376b85f 100644
--- a/core/server/server.js
+++ b/core/server/server.js
@@ -203,7 +203,6 @@ const privateConfigSchema = Joi.object({
   teamcity_pass: Joi.string(),
   twitch_client_id: Joi.string(),
   twitch_client_secret: Joi.string(),
-  wheelmap_token: Joi.string(),
   influx_username: Joi.string(),
   influx_password: Joi.string(),
   weblate_api_key: Joi.string(),
diff --git a/doc/production-hosting.md b/doc/production-hosting.md
index 9fb66593f6c57b9b87c554686510f7d0b0e5bf7f..77444e3ec765fb4ae04ac8c9318d0bf66e9e6b69 100644
--- a/doc/production-hosting.md
+++ b/doc/production-hosting.md
@@ -28,7 +28,6 @@ Production hosting is managed by the Shields ops team:
 | YouTube                       | Account owner               | @PyvesB                                                         |
 | GitLab                        | Account owner               | @calebcartwright                                                |
 | GitLab                        | Account access              | @calebcartwright, @chris48s, @paulmelnikow, @PyvesB             |
-| OpenStreetMap (for Wheelmap)  | Account owner               | @paulmelnikow                                                   |
 | DNS                           | Account owner               | @olivierlacan                                                   |
 | DNS                           | Read-only account access    | @espadrine, @paulmelnikow, @chris48s                            |
 | Sentry                        | Error reports               | @espadrine, @paulmelnikow                                       |
diff --git a/doc/server-secrets.md b/doc/server-secrets.md
index d7d66fc3cdaca2cfd389fd254cf776f0bc4abc1c..1f94579324da619e202977222adf6b1600648766 100644
--- a/doc/server-secrets.md
+++ b/doc/server-secrets.md
@@ -351,16 +351,6 @@ You can find your Weblate API key in your profile under
 [weblate authentication]: https://docs.weblate.org/en/latest/api.html#authentication-and-generic-parameters
 [weblate api key location]: https://hosted.weblate.org/accounts/profile/#api
 
-### Wheelmap
-
-- `WHEELMAP_TOKEN` (yml: `private.wheelmap_token`)
-
-The wheelmap API requires authentication. To obtain a token,
-Create an account, [sign in][wheelmap token] and use the _Authentication Token_
-displayed on your profile page.
-
-[wheelmap token]: http://classic.wheelmap.org/en/users/sign_in
-
 ### YouTube
 
 - `YOUTUBE_API_KEY` (yml: `private.youtube_api_key`)
diff --git a/services/wheelmap/wheelmap.service.js b/services/wheelmap/wheelmap.service.js
index 4a1ab308c5a5056cf29d7114a8969a26310c3b25..f296d7947845381dca4c6dd983b6d4d626936ddb 100644
--- a/services/wheelmap/wheelmap.service.js
+++ b/services/wheelmap/wheelmap.service.js
@@ -1,71 +1,11 @@
-import Joi from 'joi'
-import { BaseJsonService, pathParams } from '../index.js'
+import { deprecatedService } from '../index.js'
 
-const schema = Joi.object({
-  node: Joi.object({
-    wheelchair: Joi.string().required(),
-  }).required(),
-}).required()
-
-export default class Wheelmap extends BaseJsonService {
-  static category = 'other'
-
-  static route = {
+export const Wheelmap = deprecatedService({
+  category: 'other',
+  route: {
     base: 'wheelmap/a',
-    pattern: ':nodeId(-?[0-9]+)',
-  }
-
-  static auth = {
-    passKey: 'wheelmap_token',
-    authorizedOrigins: ['https://wheelmap.org'],
-    isRequired: true,
-  }
-
-  static openApi = {
-    '/wheelmap/a/{nodeId}': {
-      get: {
-        summary: 'Wheelmap',
-        parameters: pathParams({
-          name: 'nodeId',
-          example: '26699541',
-        }),
-      },
-    },
-  }
-
-  static defaultBadgeData = { label: 'accessibility' }
-
-  static render({ accessibility }) {
-    let color
-    if (accessibility === 'yes') {
-      color = 'brightgreen'
-    } else if (accessibility === 'limited') {
-      color = 'yellow'
-    } else if (accessibility === 'no') {
-      color = 'red'
-    }
-    return { message: accessibility, color }
-  }
-
-  async fetch({ nodeId }) {
-    return this._requestJson(
-      this.authHelper.withQueryStringAuth(
-        { passKey: 'api_key' },
-        {
-          schema,
-          url: `https://wheelmap.org/api/nodes/${nodeId}`,
-          httpErrors: {
-            401: 'invalid token',
-            404: 'node not found',
-          },
-        },
-      ),
-    )
-  }
-
-  async handle({ nodeId }) {
-    const json = await this.fetch({ nodeId })
-    const accessibility = json.node.wheelchair
-    return this.constructor.render({ accessibility })
-  }
-}
+    pattern: ':nodeId',
+  },
+  label: 'wheelmap',
+  dateAdded: new Date('2024-09-14'),
+})
diff --git a/services/wheelmap/wheelmap.spec.js b/services/wheelmap/wheelmap.spec.js
deleted file mode 100644
index b6d9f22a5e62cc940063a70950b2eed604d49609..0000000000000000000000000000000000000000
--- a/services/wheelmap/wheelmap.spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { expect } from 'chai'
-import nock from 'nock'
-import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
-import Wheelmap from './wheelmap.service.js'
-
-describe('Wheelmap', function () {
-  cleanUpNockAfterEach()
-
-  const token = 'abc123'
-  const config = { private: { wheelmap_token: token } }
-
-  function createMock({ nodeId, wheelchair }) {
-    const scope = nock('https://wheelmap.org')
-      .get(`/api/nodes/${nodeId}`)
-      .query({ api_key: token })
-
-    if (wheelchair) {
-      return scope.reply(200, { node: { wheelchair } })
-    } else {
-      return scope.reply(404)
-    }
-  }
-
-  it('node with accessibility', async function () {
-    const nodeId = '26699541'
-    const scope = createMock({ nodeId, wheelchair: 'yes' })
-    expect(
-      await Wheelmap.invoke(defaultContext, config, { nodeId }),
-    ).to.deep.equal({ message: 'yes', color: 'brightgreen' })
-    scope.done()
-  })
-
-  it('node with limited accessibility', async function () {
-    const nodeId = '2034868974'
-    const scope = createMock({ nodeId, wheelchair: 'limited' })
-    expect(
-      await Wheelmap.invoke(defaultContext, config, { nodeId }),
-    ).to.deep.equal({ message: 'limited', color: 'yellow' })
-    scope.done()
-  })
-
-  it('node without accessibility', async function () {
-    const nodeId = '-147495158'
-    const scope = createMock({ nodeId, wheelchair: 'no' })
-    expect(
-      await Wheelmap.invoke(defaultContext, config, { nodeId }),
-    ).to.deep.equal({ message: 'no', color: 'red' })
-    scope.done()
-  })
-
-  it('node not found', async function () {
-    const nodeId = '0'
-    const scope = createMock({ nodeId })
-    expect(
-      await Wheelmap.invoke(defaultContext, config, { nodeId }),
-    ).to.deep.equal({ message: 'node not found', color: 'red', isError: true })
-    scope.done()
-  })
-})
diff --git a/services/wheelmap/wheelmap.tester.js b/services/wheelmap/wheelmap.tester.js
index e4d5e6c49b580d8548ebfa2d9b164d57f6a5304e..0c9b6d262b57926df7950e91c933eb2f2955ca37 100644
--- a/services/wheelmap/wheelmap.tester.js
+++ b/services/wheelmap/wheelmap.tester.js
@@ -1,44 +1,10 @@
-import { createServiceTester } from '../tester.js'
-import { noToken } from '../test-helpers.js'
-import _noWheelmapToken from './wheelmap.service.js'
-export const t = await createServiceTester()
-const noWheelmapToken = noToken(_noWheelmapToken)
-
-t.create('node with accessibility')
-  .skipWhen(noWheelmapToken)
+import { ServiceTester } from '../tester.js'
+export const t = new ServiceTester({
+  id: 'Wheelmap',
+  title: 'Wheelmap',
+  pathPrefix: '/wheelmap/a',
+})
+
+t.create('wheelmap (deprecated)')
   .get('/26699541.json')
-  .timeout(7500)
-  .expectBadge({
-    label: 'accessibility',
-    message: 'yes',
-    color: 'brightgreen',
-  })
-
-t.create('node with limited accessibility')
-  .skipWhen(noWheelmapToken)
-  .get('/2034868974.json')
-  .timeout(7500)
-  .expectBadge({
-    label: 'accessibility',
-    message: 'limited',
-    color: 'yellow',
-  })
-
-t.create('node without accessibility')
-  .skipWhen(noWheelmapToken)
-  .get('/-147495158.json')
-  .timeout(7500)
-  .expectBadge({
-    label: 'accessibility',
-    message: 'no',
-    color: 'red',
-  })
-
-t.create('node not found')
-  .skipWhen(noWheelmapToken)
-  .get('/0.json')
-  .timeout(7500)
-  .expectBadge({
-    label: 'accessibility',
-    message: 'node not found',
-  })
+  .expectBadge({ label: 'wheelmap', message: 'no longer available' })