diff --git a/core/base-service/base-non-memory-caching.js b/core/base-service/base-non-memory-caching.js
index 9ffe066c5804c699355a198d25480e6373c131be..a988e27ee4fe38286f999f758c215381308f96cd 100644
--- a/core/base-service/base-non-memory-caching.js
+++ b/core/base-service/base-non-memory-caching.js
@@ -4,6 +4,7 @@ const makeBadge = require('../../gh-badges/lib/make-badge')
 const BaseService = require('./base')
 const { setCacheHeaders } = require('./cache-headers')
 const { makeSend } = require('./legacy-result-sender')
+const coalesceBadge = require('./coalesce-badge')
 
 // Badges are subject to two independent types of caching: in-memory and
 // downstream.
@@ -35,7 +36,13 @@ module.exports = class NonMemoryCachingBaseService extends BaseService {
         queryParams
       )
 
-      const badgeData = this._makeBadgeData(queryParams, serviceData)
+      const badgeData = coalesceBadge(
+        queryParams,
+        serviceData,
+        this.defaultBadgeData,
+        this
+      )
+
       // The final capture group is the extension.
       const format = match.slice(-1)[0]
       badgeData.format = format
diff --git a/core/base-service/base-static.js b/core/base-service/base-static.js
index d9afd34672cc6db0b8d89c868376fc930a41d59e..8f27cde417a20960fefa6187aa0eeab5b71ef551 100644
--- a/core/base-service/base-static.js
+++ b/core/base-service/base-static.js
@@ -8,6 +8,7 @@ const {
   setCacheHeadersForStaticResource,
 } = require('./cache-headers')
 const { makeSend } = require('./legacy-result-sender')
+const coalesceBadge = require('./coalesce-badge')
 
 module.exports = class BaseStaticService extends BaseService {
   static register({ camp }, serviceConfig) {
@@ -33,7 +34,13 @@ module.exports = class BaseStaticService extends BaseService {
         queryParams
       )
 
-      const badgeData = this._makeBadgeData(queryParams, serviceData)
+      const badgeData = coalesceBadge(
+        queryParams,
+        serviceData,
+        this.defaultBadgeData,
+        this
+      )
+
       // The final capture group is the extension.
       const format = match.slice(-1)[0]
       badgeData.format = format
diff --git a/core/base-service/base.js b/core/base-service/base.js
index e9a6485022134465a3e7fdcfa3b91e759cfe44f6..92b1efb7952e2bd8a6a7487a27d11ba5ce148041 100644
--- a/core/base-service/base.js
+++ b/core/base-service/base.js
@@ -5,14 +5,8 @@ const emojic = require('emojic')
 const pathToRegexp = require('path-to-regexp')
 const Joi = require('joi')
 const { checkErrorResponse } = require('../../lib/error-helper')
-const { toArray } = require('../../lib/badge-data')
-const { svg2base64 } = require('../../lib/svg-helpers')
-const {
-  decodeDataUrlFromQueryParam,
-  prepareNamedLogo,
-} = require('../../lib/logos')
 const { assertValidCategory } = require('../../services/categories')
-const coalesce = require('./coalesce')
+const coalesceBadge = require('./coalesce-badge')
 const {
   NotFound,
   InvalidResponse,
@@ -348,143 +342,6 @@ class BaseService {
     return serviceData
   }
 
-  // Translate modern badge data to the legacy schema understood by the badge
-  // maker. Allow the user to override the label, color, logo, etc. through
-  // the query string. Provide support for most badge options via
-  // `serviceData` so the Endpoint badge can specify logos and colors, though
-  // allow that the user's logo or color to take precedence. A notable
-  // exception is the case of errors. When the service specifies that an error
-  // has occurred, the user's requested color does not override the error color.
-  //
-  // Logos are resolved in this manner:
-  //
-  // 1. When `?logo=` contains the name of one of the Shields logos, or contains
-  //    base64-encoded SVG, that logo is used. In the case of a named logo, when
-  //    a `&logoColor=` is specified, that color is used. Otherwise the default
-  //    color is used. `logoColor` will not be applied to a custom
-  //    (base64-encoded) logo; if a custom color is desired the logo should be
-  //    recolored prior to making the request. The appearance of the logo can be
-  //    customized using `logoWidth`, and in the case of the popout badge,
-  //    `logoPosition`. When `?logo=` is specified, any logo-related parameters
-  //    specified dynamically by the service, or by default in the service, are
-  //    ignored.
-  // 2. The second precedence is the dynamic logo returned by a service. This is
-  //    used only by the Endpoint badge. The `logoColor` can be overridden by the
-  //    query string.
-  // 3. In the case of the `social` style only, the last precedence is the
-  //    service's default logo. The `logoColor` can be overridden by the query
-  //    string.
-  static _makeBadgeData(overrides, serviceData) {
-    const {
-      style: overrideStyle,
-      label: overrideLabel,
-      logoColor: overrideLogoColor,
-      link: overrideLink,
-    } = overrides
-    // Scoutcamp converts numeric query params to numbers. Convert them back.
-    let {
-      colorB: overrideColor,
-      colorA: overrideLabelColor,
-      logoWidth: overrideLogoWidth,
-      logoPosition: overrideLogoPosition,
-    } = overrides
-    if (typeof overrideColor === 'number') {
-      overrideColor = `${overrideColor}`
-    }
-    if (typeof overrideLabelColor === 'number') {
-      overrideLabelColor = `${overrideLabelColor}`
-    }
-    overrideLogoWidth = +overrideLogoWidth || undefined
-    overrideLogoPosition = +overrideLogoPosition || undefined
-    // `?logo=` could be a named logo or encoded svg. Split up these cases.
-    const overrideLogoSvgBase64 = decodeDataUrlFromQueryParam(overrides.logo)
-    const overrideNamedLogo = overrideLogoSvgBase64 ? undefined : overrides.logo
-
-    const {
-      isError,
-      label: serviceLabel,
-      message: serviceMessage,
-      color: serviceColor,
-      labelColor: serviceLabelColor,
-      logoSvg: serviceLogoSvg,
-      namedLogo: serviceNamedLogo,
-      logoColor: serviceLogoColor,
-      logoWidth: serviceLogoWidth,
-      logoPosition: serviceLogoPosition,
-      link: serviceLink,
-      cacheSeconds: serviceCacheSeconds,
-      style: serviceStyle,
-    } = serviceData
-    const serviceLogoSvgBase64 = serviceLogoSvg
-      ? svg2base64(serviceLogoSvg)
-      : undefined
-
-    const {
-      color: defaultColor,
-      namedLogo: defaultNamedLogo,
-      label: defaultLabel,
-      labelColor: defaultLabelColor,
-    } = this.defaultBadgeData
-    const defaultCacheSeconds = this._cacheLength
-
-    const style = coalesce(overrideStyle, serviceStyle)
-
-    const namedLogoSvgBase64 = prepareNamedLogo({
-      name: coalesce(
-        overrideNamedLogo,
-        serviceNamedLogo,
-        style === 'social' ? defaultNamedLogo : undefined
-      ),
-      color: coalesce(
-        overrideLogoColor,
-        // If the logo has been overridden it does not make sense to inherit
-        // the color.
-        overrideNamedLogo ? undefined : serviceLogoColor
-      ),
-      style,
-    })
-
-    return {
-      text: [
-        // Use `coalesce()` to support empty labels and messages, as in the
-        // static badge.
-        coalesce(overrideLabel, serviceLabel, defaultLabel, this.category),
-        coalesce(serviceMessage, 'n/a'),
-      ],
-      color: coalesce(
-        // In case of an error, disregard user's color override.
-        isError ? undefined : overrideColor,
-        serviceColor,
-        defaultColor,
-        'lightgrey'
-      ),
-      labelColor: coalesce(
-        // In case of an error, disregard user's color override.
-        isError ? undefined : overrideLabelColor,
-        serviceLabelColor,
-        defaultLabelColor
-      ),
-      template: style,
-      logo: coalesce(
-        overrideLogoSvgBase64,
-        serviceLogoSvgBase64,
-        namedLogoSvgBase64
-      ),
-      logoWidth: coalesce(
-        overrideLogoWidth,
-        // If the logo has been overridden it does not make sense to inherit
-        // the width or position.
-        overrideNamedLogo ? undefined : serviceLogoWidth
-      ),
-      logoPosition: coalesce(
-        overrideLogoPosition,
-        overrideNamedLogo ? undefined : serviceLogoPosition
-      ),
-      links: toArray(overrideLink || serviceLink),
-      cacheLengthSeconds: coalesce(serviceCacheSeconds, defaultCacheSeconds),
-    }
-  }
-
   static register({ camp, handleRequest, githubApiProvider }, serviceConfig) {
     const { cacheHeaders: cacheHeaderConfig, fetchLimitBytes } = serviceConfig
     camp.route(
@@ -504,7 +361,12 @@ class BaseService {
             queryParams
           )
 
-          const badgeData = this._makeBadgeData(queryParams, serviceData)
+          const badgeData = coalesceBadge(
+            queryParams,
+            serviceData,
+            this.defaultBadgeData,
+            this
+          )
           // The final capture group is the extension.
           const format = match.slice(-1)[0]
           sendBadge(format, badgeData)
diff --git a/core/base-service/base.spec.js b/core/base-service/base.spec.js
index c8df17ade48cb35c1b1df43385f509977cf9f1fd..d6062b153a241087b1e3e3a5c158169ab49b5797 100644
--- a/core/base-service/base.spec.js
+++ b/core/base-service/base.spec.js
@@ -4,7 +4,6 @@ const Joi = require('joi')
 const { expect } = require('chai')
 const { test, given, forCases } = require('sazerac')
 const sinon = require('sinon')
-const { getShieldsIcon } = require('../../lib/logos')
 const trace = require('./trace')
 
 const {
@@ -371,245 +370,6 @@ describe('BaseService', function() {
     })
   })
 
-  describe('_makeBadgeData', function() {
-    describe('Overrides', function() {
-      it('overrides the label', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { label: 'purr count' },
-          { label: 'purrs' }
-        )
-        expect(badgeData.text).to.deep.equal(['purr count', 'n/a'])
-      })
-
-      it('overrides the label color', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { colorA: '42f483' },
-          { color: 'green' }
-        )
-        expect(badgeData.labelColor).to.equal('42f483')
-      })
-
-      it('overrides the color', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { colorB: '10ADED' },
-          { color: 'red' }
-        )
-        expect(badgeData.color).to.equal('10ADED')
-      })
-
-      it('converts a query-string numeric color to a string', function() {
-        const badgeData = DummyService._makeBadgeData(
-          // Scoutcamp converts numeric query params to numbers.
-          { colorB: 123 },
-          { color: 'green' }
-        )
-        expect(badgeData.color).to.equal('123')
-      })
-
-      it('does not override the color in case of an error', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { colorB: '10ADED' },
-          { isError: true, color: 'lightgray' }
-        )
-        expect(badgeData.color).to.equal('lightgray')
-      })
-
-      it('overrides the logo', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { logo: 'github' },
-          { namedLogo: 'appveyor' }
-        )
-        // .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`.
-        expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'github' })).and
-          .not.be.empty
-      })
-
-      it('overrides the logo with a color', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { logo: 'github', logoColor: 'blue' },
-          { namedLogo: 'appveyor' }
-        )
-        expect(badgeData.logo).to.equal(
-          getShieldsIcon({ name: 'github', color: 'blue' })
-        ).and.not.be.empty
-      })
-
-      it("when the logo is overridden, it ignores the service's logo color, position, and width", function() {
-        const badgeData = DummyService._makeBadgeData(
-          { logo: 'github' },
-          {
-            namedLogo: 'appveyor',
-            logoColor: 'red',
-            logoPosition: -3,
-            logoWidth: 100,
-          }
-        )
-        expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'github' })).and
-          .not.be.empty
-      })
-
-      it("overrides the service logo's color", function() {
-        const badgeData = DummyService._makeBadgeData(
-          { logoColor: 'blue' },
-          { namedLogo: 'github', logoColor: 'red' }
-        )
-        expect(badgeData.logo).to.equal(
-          getShieldsIcon({ name: 'github', color: 'blue' })
-        ).and.not.be.empty
-      })
-
-      it('overrides the logo with custom svg', function() {
-        const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
-        const badgeData = DummyService._makeBadgeData(
-          { logo: logoSvg },
-          { namedLogo: 'appveyor' }
-        )
-        expect(badgeData.logo).to.equal(logoSvg)
-      })
-
-      it('ignores the color when custom svg is provided', function() {
-        const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
-        const badgeData = DummyService._makeBadgeData(
-          { logo: logoSvg, logoColor: 'brightgreen' },
-          { namedLogo: 'appveyor' }
-        )
-        expect(badgeData.logo).to.equal(logoSvg)
-      })
-
-      it('overrides the logoWidth', function() {
-        const badgeData = DummyService._makeBadgeData({ logoWidth: 20 }, {})
-        expect(badgeData.logoWidth).to.equal(20)
-      })
-
-      it('overrides the logoPosition', function() {
-        const badgeData = DummyService._makeBadgeData({ logoPosition: -10 }, {})
-        expect(badgeData.logoPosition).to.equal(-10)
-      })
-
-      it('overrides the links', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { link: 'https://circleci.com/gh/badges/daily-tests' },
-          {
-            link:
-              'https://circleci.com/workflow-run/184ef3de-4836-4805-a2e4-0ceba099f92d',
-          }
-        )
-        expect(badgeData.links).to.deep.equal([
-          'https://circleci.com/gh/badges/daily-tests',
-        ])
-      })
-
-      it('overrides the template', function() {
-        const badgeData = DummyService._makeBadgeData({ style: 'pill' }, {})
-        expect(badgeData.template).to.equal('pill')
-      })
-
-      it('overrides the cache length', function() {
-        const badgeData = DummyService._makeBadgeData(
-          { style: 'pill' },
-          { cacheSeconds: 123 }
-        )
-        expect(badgeData.cacheLengthSeconds).to.equal(123)
-      })
-    })
-
-    describe('Service data', function() {
-      it('applies the service message', function() {
-        const badgeData = DummyService._makeBadgeData({}, { message: '10k' })
-        expect(badgeData.text).to.deep.equal(['cat', '10k'])
-      })
-
-      it('preserves an empty label', function() {
-        const badgeData = DummyService._makeBadgeData(
-          {},
-          { label: '', message: '10k' }
-        )
-        expect(badgeData.text).to.deep.equal(['', '10k'])
-      })
-
-      it('applies a numeric service message', function() {
-        // While a number of badges use this, in the long run we may want
-        // `render()` to always return a string.
-        const badgeData = DummyService._makeBadgeData({}, { message: 10 })
-        expect(badgeData.text).to.deep.equal(['cat', 10])
-      })
-
-      it('applies the service color', function() {
-        const badgeData = DummyService._makeBadgeData({}, { color: 'red' })
-        expect(badgeData.color).to.equal('red')
-      })
-
-      it('applies the named logo', function() {
-        const badgeData = DummyService._makeBadgeData(
-          {},
-          { namedLogo: 'github' }
-        )
-        // .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`.
-        expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'github' })).and
-          .not.to.be.empty
-      })
-
-      it('applies the named logo with color', function() {
-        const badgeData = DummyService._makeBadgeData(
-          {},
-          { namedLogo: 'github', logoColor: 'blue' }
-        )
-        expect(badgeData.logo).to.equal(
-          getShieldsIcon({ name: 'github', color: 'blue' })
-        ).and.not.to.be.empty
-      })
-
-      it('applies the logo width', function() {
-        const badgeData = DummyService._makeBadgeData(
-          {},
-          { namedLogo: 'github', logoWidth: 275 }
-        )
-        expect(badgeData.logoWidth).to.equal(275)
-      })
-
-      it('applies the logo position', function() {
-        const badgeData = DummyService._makeBadgeData(
-          {},
-          { namedLogo: 'github', logoPosition: -10 }
-        )
-        expect(badgeData.logoPosition).to.equal(-10)
-      })
-
-      it('applies the service label color', function() {
-        const badgeData = DummyService._makeBadgeData({}, { labelColor: 'red' })
-        expect(badgeData.labelColor).to.equal('red')
-      })
-    })
-
-    describe('Defaults', function() {
-      it('uses the default label', function() {
-        const badgeData = DummyService._makeBadgeData({}, {})
-        expect(badgeData.text).to.deep.equal(['cat', 'n/a'])
-      })
-
-      it('uses the default color', function() {
-        const badgeData = DummyService._makeBadgeData({}, {})
-        expect(badgeData.color).to.equal('lightgrey')
-      })
-
-      it('provides no default label color', function() {
-        const badgeData = DummyService._makeBadgeData({}, {})
-        expect(badgeData.labelColor).to.be.undefined
-      })
-
-      it('when not a social badge, ignores the default named logo', function() {
-        const badgeData = DummyService._makeBadgeData({}, {})
-        expect(badgeData.logo).to.be.undefined
-      })
-
-      it('when a social badge, uses the default named logo', function() {
-        const badgeData = DummyService._makeBadgeData({ style: 'social' }, {})
-        expect(badgeData.logo).to.equal(getShieldsIcon({ name: 'appveyor' }))
-          .and.not.be.empty
-      })
-    })
-  })
-
   describe('ScoutCamp integration', function() {
     const expectedRouteRegex = /^\/foo\/([^/]+?)\.(svg|png|gif|jpg|json)$/
 
diff --git a/core/base-service/coalesce-badge.js b/core/base-service/coalesce-badge.js
new file mode 100644
index 0000000000000000000000000000000000000000..2b9dae2639ace43df3a73f915d04ee4bca54da89
--- /dev/null
+++ b/core/base-service/coalesce-badge.js
@@ -0,0 +1,151 @@
+'use strict'
+
+const {
+  decodeDataUrlFromQueryParam,
+  prepareNamedLogo,
+} = require('../../lib/logos')
+const { toArray } = require('../../lib/badge-data')
+const { svg2base64 } = require('../../lib/svg-helpers')
+const coalesce = require('./coalesce')
+
+// Translate modern badge data to the legacy schema understood by the badge
+// maker. Allow the user to override the label, color, logo, etc. through the
+// query string. Provide support for most badge options via `serviceData` so
+// the Endpoint badge can specify logos and colors, though allow that the
+// user's logo or color to take precedence. A notable exception is the case of
+// errors. When the service specifies that an error has occurred, the user's
+// requested color does not override the error color.
+//
+// Logos are resolved in this manner:
+//
+// 1. When `?logo=` contains the name of one of the Shields logos, or contains
+//    base64-encoded SVG, that logo is used. In the case of a named logo, when
+//    a `&logoColor=` is specified, that color is used. Otherwise the default
+//    color is used. `logoColor` will not be applied to a custom
+//    (base64-encoded) logo; if a custom color is desired the logo should be
+//    recolored prior to making the request. The appearance of the logo can be
+//    customized using `logoWidth`, and in the case of the popout badge,
+//    `logoPosition`. When `?logo=` is specified, any logo-related parameters
+//    specified dynamically by the service, or by default in the service, are
+//    ignored.
+// 2. The second precedence is the dynamic logo returned by a service. This is
+//    used only by the Endpoint badge. The `logoColor` can be overridden by the
+//    query string.
+// 3. In the case of the `social` style only, the last precedence is the
+//    service's default logo. The `logoColor` can be overridden by the query
+//    string.
+module.exports = function coalesceBadge(
+  overrides,
+  serviceData,
+  // These two parameters were kept separate to make tests clearer.
+  defaultBadgeData,
+  { category, _cacheLength: defaultCacheSeconds } = {}
+) {
+  const {
+    style: overrideStyle,
+    label: overrideLabel,
+    logoColor: overrideLogoColor,
+    link: overrideLink,
+  } = overrides
+  // Scoutcamp converts numeric query params to numbers. Convert them back.
+  let {
+    colorB: overrideColor,
+    colorA: overrideLabelColor,
+    logoWidth: overrideLogoWidth,
+    logoPosition: overrideLogoPosition,
+  } = overrides
+  if (typeof overrideColor === 'number') {
+    overrideColor = `${overrideColor}`
+  }
+  if (typeof overrideLabelColor === 'number') {
+    overrideLabelColor = `${overrideLabelColor}`
+  }
+  overrideLogoWidth = +overrideLogoWidth || undefined
+  overrideLogoPosition = +overrideLogoPosition || undefined
+  // `?logo=` could be a named logo or encoded svg. Split up these cases.
+  const overrideLogoSvgBase64 = decodeDataUrlFromQueryParam(overrides.logo)
+  const overrideNamedLogo = overrideLogoSvgBase64 ? undefined : overrides.logo
+
+  const {
+    isError,
+    label: serviceLabel,
+    message: serviceMessage,
+    color: serviceColor,
+    labelColor: serviceLabelColor,
+    logoSvg: serviceLogoSvg,
+    namedLogo: serviceNamedLogo,
+    logoColor: serviceLogoColor,
+    logoWidth: serviceLogoWidth,
+    logoPosition: serviceLogoPosition,
+    link: serviceLink,
+    cacheSeconds: serviceCacheSeconds,
+    style: serviceStyle,
+  } = serviceData
+  const serviceLogoSvgBase64 = serviceLogoSvg
+    ? svg2base64(serviceLogoSvg)
+    : undefined
+
+  const {
+    color: defaultColor,
+    namedLogo: defaultNamedLogo,
+    label: defaultLabel,
+    labelColor: defaultLabelColor,
+  } = defaultBadgeData
+
+  const style = coalesce(overrideStyle, serviceStyle)
+
+  const namedLogoSvgBase64 = prepareNamedLogo({
+    name: coalesce(
+      overrideNamedLogo,
+      serviceNamedLogo,
+      style === 'social' ? defaultNamedLogo : undefined
+    ),
+    color: coalesce(
+      overrideLogoColor,
+      // If the logo has been overridden it does not make sense to inherit
+      // the color.
+      overrideNamedLogo ? undefined : serviceLogoColor
+    ),
+    style,
+  })
+
+  return {
+    text: [
+      // Use `coalesce()` to support empty labels and messages, as in the
+      // static badge.
+      coalesce(overrideLabel, serviceLabel, defaultLabel, category),
+      coalesce(serviceMessage, 'n/a'),
+    ],
+    color: coalesce(
+      // In case of an error, disregard user's color override.
+      isError ? undefined : overrideColor,
+      serviceColor,
+      defaultColor,
+      'lightgrey'
+    ),
+    labelColor: coalesce(
+      // In case of an error, disregard user's color override.
+      isError ? undefined : overrideLabelColor,
+      serviceLabelColor,
+      defaultLabelColor
+    ),
+    template: style,
+    logo: coalesce(
+      overrideLogoSvgBase64,
+      serviceLogoSvgBase64,
+      namedLogoSvgBase64
+    ),
+    logoWidth: coalesce(
+      overrideLogoWidth,
+      // If the logo has been overridden it does not make sense to inherit
+      // the width or position.
+      overrideNamedLogo ? undefined : serviceLogoWidth
+    ),
+    logoPosition: coalesce(
+      overrideLogoPosition,
+      overrideNamedLogo ? undefined : serviceLogoPosition
+    ),
+    links: toArray(overrideLink || serviceLink),
+    cacheLengthSeconds: coalesce(serviceCacheSeconds, defaultCacheSeconds),
+  }
+}
diff --git a/core/base-service/coalesce-badge.spec.js b/core/base-service/coalesce-badge.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c9ba4ba060f3a7aae7b0d40fd0bad895a837685
--- /dev/null
+++ b/core/base-service/coalesce-badge.spec.js
@@ -0,0 +1,257 @@
+'use strict'
+
+const { expect } = require('chai')
+const { getShieldsIcon } = require('../../lib/logos')
+const coalesceBadge = require('./coalesce-badge')
+
+describe('coalesceBadge', function() {
+  describe('Label', function() {
+    it('uses the default label', function() {
+      expect(coalesceBadge({}, {}, { label: 'heyo' }).text).to.deep.equal([
+        'heyo',
+        'n/a',
+      ])
+    })
+
+    // This behavior isn't great and we might want to remove it.
+    it('uses the category as a default label', function() {
+      expect(coalesceBadge({}, {}, {}, { category: 'cat' }).text).to.deep.equal(
+        ['cat', 'n/a']
+      )
+    })
+
+    it('preserves an empty label', function() {
+      expect(
+        coalesceBadge({}, { label: '', message: '10k' }, {}).text
+      ).to.deep.equal(['', '10k'])
+    })
+
+    it('overrides the label', function() {
+      expect(
+        coalesceBadge({ label: 'purr count' }, { label: 'purrs' }, {}).text
+      ).to.deep.equal(['purr count', 'n/a'])
+    })
+  })
+
+  describe('Message', function() {
+    it('applies the service message', function() {
+      expect(coalesceBadge({}, { message: '10k' }, {}).text).to.deep.equal([
+        undefined,
+        '10k',
+      ])
+    })
+
+    it('applies a numeric service message', function() {
+      // While a number of badges use this, in the long run we may want
+      // `render()` to always return a string.
+      expect(coalesceBadge({}, { message: 10 }, {}).text).to.deep.equal([
+        undefined,
+        10,
+      ])
+    })
+  })
+
+  describe('Right color', function() {
+    it('uses the default color', function() {
+      expect(coalesceBadge({}, {}, {}).color).to.equal('lightgrey')
+    })
+
+    it('overrides the color', function() {
+      expect(
+        coalesceBadge({ colorB: '10ADED' }, { color: 'red' }, {}).color
+      ).to.equal('10ADED')
+    })
+
+    context('In case of an error', function() {
+      it('does not override the color', function() {
+        expect(
+          coalesceBadge(
+            { colorB: '10ADED' },
+            { isError: true, color: 'lightgray' },
+            {}
+          ).color
+        ).to.equal('lightgray')
+      })
+    })
+
+    it('applies the service color', function() {
+      expect(coalesceBadge({}, { color: 'red' }, {}).color).to.equal('red')
+    })
+  })
+
+  describe('Left color', function() {
+    it('provides no default label color', function() {
+      expect(coalesceBadge({}, {}, {}).labelColor).to.be.undefined
+    })
+
+    it('applies the service label color', function() {
+      expect(coalesceBadge({}, { labelColor: 'red' }, {}).labelColor).to.equal(
+        'red'
+      )
+    })
+
+    it('overrides the label color', function() {
+      expect(
+        coalesceBadge({ colorA: '42f483' }, { color: 'green' }, {}).labelColor
+      ).to.equal('42f483')
+    })
+
+    it('converts a query-string numeric color to a string', function() {
+      expect(
+        coalesceBadge(
+          // Scoutcamp converts numeric query params to numbers.
+          { colorB: 123 },
+          { color: 'green' },
+          {}
+        ).color
+      ).to.equal('123')
+    })
+  })
+
+  describe('Named logos', function() {
+    it('when not a social badge, ignores the default named logo', function() {
+      expect(coalesceBadge({}, {}, { namedLogo: 'appveyor' }).logo).to.be
+        .undefined
+    })
+
+    it('when a social badge, uses the default named logo', function() {
+      // .not.be.empty for confidence that nothing has changed with `getShieldsIcon()`.
+      expect(
+        coalesceBadge({ style: 'social' }, {}, { namedLogo: 'appveyor' }).logo
+      ).to.equal(getShieldsIcon({ name: 'appveyor' })).and.not.be.empty
+    })
+
+    it('applies the named logo', function() {
+      expect(coalesceBadge({}, { namedLogo: 'github' }, {}).logo).to.equal(
+        getShieldsIcon({ name: 'github' })
+      ).and.not.to.be.empty
+    })
+
+    it('applies the named logo with color', function() {
+      expect(
+        coalesceBadge({}, { namedLogo: 'github', logoColor: 'blue' }, {}).logo
+      ).to.equal(getShieldsIcon({ name: 'github', color: 'blue' })).and.not.to
+        .be.empty
+    })
+
+    it('overrides the logo', function() {
+      expect(
+        coalesceBadge({ logo: 'github' }, { namedLogo: 'appveyor' }, {}).logo
+      ).to.equal(getShieldsIcon({ name: 'github' })).and.not.be.empty
+    })
+
+    it('overrides the logo with a color', function() {
+      expect(
+        coalesceBadge(
+          { logo: 'github', logoColor: 'blue' },
+          { namedLogo: 'appveyor' },
+          {}
+        ).logo
+      ).to.equal(getShieldsIcon({ name: 'github', color: 'blue' })).and.not.be
+        .empty
+    })
+
+    it("when the logo is overridden, it ignores the service's logo color, position, and width", function() {
+      expect(
+        coalesceBadge(
+          { logo: 'github' },
+          {
+            namedLogo: 'appveyor',
+            logoColor: 'red',
+            logoPosition: -3,
+            logoWidth: 100,
+          },
+          {}
+        ).logo
+      ).to.equal(getShieldsIcon({ name: 'github' })).and.not.be.empty
+    })
+
+    it("overrides the service logo's color", function() {
+      expect(
+        coalesceBadge(
+          { logoColor: 'blue' },
+          { namedLogo: 'github', logoColor: 'red' },
+          {}
+        ).logo
+      ).to.equal(getShieldsIcon({ name: 'github', color: 'blue' })).and.not.be
+        .empty
+    })
+  })
+
+  describe('Custom logos', function() {
+    it('overrides the logo with custom svg', function() {
+      const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
+      expect(
+        coalesceBadge({ logo: logoSvg }, { namedLogo: 'appveyor' }, {}).logo
+      ).to.equal(logoSvg)
+    })
+
+    it('ignores the color when custom svg is provided', function() {
+      const logoSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxu'
+      expect(
+        coalesceBadge(
+          { logo: logoSvg, logoColor: 'brightgreen' },
+          { namedLogo: 'appveyor' },
+          {}
+        ).logo
+      ).to.equal(logoSvg)
+    })
+  })
+
+  describe('Logo width', function() {
+    it('overrides the logoWidth', function() {
+      expect(coalesceBadge({ logoWidth: 20 }, {}, {}).logoWidth).to.equal(20)
+    })
+
+    it('applies the logo width', function() {
+      expect(
+        coalesceBadge({}, { namedLogo: 'github', logoWidth: 275 }, {}).logoWidth
+      ).to.equal(275)
+    })
+  })
+
+  describe('Logo position', function() {
+    it('overrides the logoPosition', function() {
+      expect(
+        coalesceBadge({ logoPosition: -10 }, {}, {}).logoPosition
+      ).to.equal(-10)
+    })
+
+    it('applies the logo position', function() {
+      expect(
+        coalesceBadge({}, { namedLogo: 'github', logoPosition: -10 }, {})
+          .logoPosition
+      ).to.equal(-10)
+    })
+  })
+
+  describe('Links', function() {
+    it('overrides the links', function() {
+      expect(
+        coalesceBadge(
+          { link: 'https://circleci.com/gh/badges/daily-tests' },
+          {
+            link:
+              'https://circleci.com/workflow-run/184ef3de-4836-4805-a2e4-0ceba099f92d',
+          },
+          {}
+        ).links
+      ).to.deep.equal(['https://circleci.com/gh/badges/daily-tests'])
+    })
+  })
+
+  describe('Style', function() {
+    it('overrides the template', function() {
+      expect(coalesceBadge({ style: 'pill' }, {}, {}).template).to.equal('pill')
+    })
+  })
+
+  describe('Cache length', function() {
+    it('overrides the cache length', function() {
+      expect(
+        coalesceBadge({ style: 'pill' }, { cacheSeconds: 123 }, {})
+          .cacheLengthSeconds
+      ).to.equal(123)
+    })
+  })
+})
diff --git a/core/base-service/transform-example.js b/core/base-service/transform-example.js
index f6bed3930d3321c49990fc63dcec49316ddf693d..1f803eb19d097daad9584c74b5703a3f8da58307 100644
--- a/core/base-service/transform-example.js
+++ b/core/base-service/transform-example.js
@@ -2,6 +2,7 @@
 
 const Joi = require('joi')
 const pathToRegexp = require('path-to-regexp')
+const coalesceBadge = require('./coalesce-badge')
 
 const optionalObjectOfKeyValues = Joi.object().pattern(
   /./,
@@ -142,7 +143,12 @@ function transformExample(inExample, index, ServiceClass) {
     const {
       text: [label, message],
       color,
-    } = ServiceClass._makeBadgeData({}, staticPreview)
+    } = coalesceBadge(
+      {},
+      staticPreview,
+      ServiceClass.defaultBadgeData,
+      ServiceClass
+    )
     preview = { label, message: `${message}`, color }
   } else {
     preview = {