diff --git a/services/coincap/coincap-base.js b/services/coincap/coincap-base.js
new file mode 100644
index 0000000000000000000000000000000000000000..7ad3dfb6c6f2ff36803eed37b3e7b559c7a29b38
--- /dev/null
+++ b/services/coincap/coincap-base.js
@@ -0,0 +1,22 @@
+import { BaseJsonService } from '../index.js'
+
+export default class BaseCoincapService extends BaseJsonService {
+  static category = 'other'
+
+  static defaultBadgeData = { label: 'coincap' }
+
+  // Doc this API. From https://docs.coincap.io/
+  // example: https://api.coincap.io/v2/assets/bitcoin
+
+  async fetch({ assetId, schema }) {
+    return this._requestJson({
+      schema,
+      url: `https://api.coincap.io/v2/assets/${assetId}`,
+      errorMessages: {
+        404: 'asset not found',
+      },
+    })
+  }
+}
+
+export { BaseCoincapService }
diff --git a/services/coincap/coincap-changepercent24hr.service.js b/services/coincap/coincap-changepercent24hr.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..29b853b8d499e5acbab297fb124e0916fb6b5b2a
--- /dev/null
+++ b/services/coincap/coincap-changepercent24hr.service.js
@@ -0,0 +1,44 @@
+import Joi from 'joi'
+import { floorCount } from '../color-formatters.js'
+import BaseCoincapService from './coincap-base.js'
+
+const schema = Joi.object({
+  data: Joi.object({
+    changePercent24Hr: Joi.string()
+      .pattern(/[0-9]*\.[0-9]+/i)
+      .required(),
+    name: Joi.string().required(),
+  }).required(),
+}).required()
+
+export default class CoincapChangePercent24HrUsd extends BaseCoincapService {
+  static route = { base: 'coincap/change-percent-24hr', pattern: ':assetId' }
+
+  static examples = [
+    {
+      title: 'Coincap (Change Percent 24Hr)',
+      namedParams: { assetId: 'bitcoin' },
+      staticPreview: this.render({
+        asset: { name: 'bitcoin', changePercent24Hr: '2.0670573674501840"' },
+      }),
+      keywords: ['bitcoin', 'crypto', 'cryptocurrency'],
+    },
+  ]
+
+  static percentFormat(changePercent24Hr) {
+    return `${parseInt(changePercent24Hr).toFixed(2)}%`
+  }
+
+  static render({ asset }) {
+    return {
+      label: `${asset.name}`.toLowerCase(),
+      message: this.percentFormat(asset.changePercent24Hr),
+      color: floorCount(asset.changePercent24Hr),
+    }
+  }
+
+  async handle({ assetId }) {
+    const { data: asset } = await this.fetch({ assetId, schema })
+    return this.constructor.render({ asset })
+  }
+}
diff --git a/services/coincap/coincap-changepercent24hr.tester.js b/services/coincap/coincap-changepercent24hr.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..564553d289e0d04a6572126edee867ab0ef1884e
--- /dev/null
+++ b/services/coincap/coincap-changepercent24hr.tester.js
@@ -0,0 +1,43 @@
+import { isPercentage } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('request for existing asset with positive')
+  .get('/bitcoin.json')
+  .intercept(nock =>
+    nock('https://api.coincap.io')
+      .get('/v2/assets/bitcoin')
+      .reply(200, {
+        data: { changePercent24Hr: '1.4767080598737783', name: 'Bitcoin' },
+      })
+  )
+  .expectBadge({
+    label: 'bitcoin',
+    message: '1.00%',
+    color: 'brightgreen',
+  })
+
+t.create('request for existing asset with negative')
+  .get('/bitcoin.json')
+  .intercept(nock =>
+    nock('https://api.coincap.io')
+      .get('/v2/assets/bitcoin')
+      .reply(200, {
+        data: { changePercent24Hr: '-1.4767080598737783', name: 'Bitcoin' },
+      })
+  )
+  .expectBadge({
+    label: 'bitcoin',
+    message: '-1.00%',
+    color: 'red',
+  })
+
+t.create('change percent 24hr').get('/bitcoin.json').expectBadge({
+  label: 'bitcoin',
+  message: isPercentage,
+})
+
+t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({
+  label: 'coincap',
+  message: 'asset not found',
+})
diff --git a/services/coincap/coincap-priceusd.service.js b/services/coincap/coincap-priceusd.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..94c1e477c435f29367d9218e73a3ff7f4a2a547c
--- /dev/null
+++ b/services/coincap/coincap-priceusd.service.js
@@ -0,0 +1,45 @@
+import Joi from 'joi'
+import BaseCoincapService from './coincap-base.js'
+
+const schema = Joi.object({
+  data: Joi.object({
+    priceUsd: Joi.string()
+      .pattern(/[0-9]*\.[0-9]+/i)
+      .required(),
+    name: Joi.string().required(),
+  }).required(),
+}).required()
+
+export default class CoincapPriceUsd extends BaseCoincapService {
+  static route = { base: 'coincap/price-usd', pattern: ':assetId' }
+
+  static examples = [
+    {
+      title: 'Coincap (Price USD)',
+      namedParams: { assetId: 'bitcoin' },
+      staticPreview: this.render({
+        asset: { name: 'bitcoin', priceUsd: '19116.0479117336250772' },
+      }),
+      keywords: ['bitcoin', 'crypto', 'cryptocurrency'],
+    },
+  ]
+
+  static priceFormat(price) {
+    return `$${parseFloat(price)
+      .toFixed(2)
+      .replace(/\d(?=(\d{3})+\.)/g, '$&,')}`
+  }
+
+  static render({ asset }) {
+    return {
+      label: `${asset.name}`.toLowerCase(),
+      message: this.priceFormat(asset.priceUsd),
+      color: 'blue',
+    }
+  }
+
+  async handle({ assetId }) {
+    const { data: asset } = await this.fetch({ assetId, schema })
+    return this.constructor.render({ asset })
+  }
+}
diff --git a/services/coincap/coincap-priceusd.spec.js b/services/coincap/coincap-priceusd.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c099a06067aa7d2503d8833ec302326df6c04919
--- /dev/null
+++ b/services/coincap/coincap-priceusd.spec.js
@@ -0,0 +1,16 @@
+import { test, given } from 'sazerac'
+import CoincapPriceUsd from './coincap-priceusd.service.js'
+
+describe('PriceUsd Format', function () {
+  test(CoincapPriceUsd.priceFormat, () => {
+    given('3').expect('$3.00')
+    given('33').expect('$33.00')
+    given('332').expect('$332.00')
+    given('3324').expect('$3,324.00')
+    given('332432').expect('$332,432.00')
+    given('332432.2').expect('$332,432.20')
+    given('332432.25').expect('$332,432.25')
+    given('332432432').expect('$332,432,432.00')
+    given('332432432.3432432').expect('$332,432,432.34')
+  })
+})
diff --git a/services/coincap/coincap-priceusd.tester.js b/services/coincap/coincap-priceusd.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..bb7acec283b4149077120796f803f3c8989c81cf
--- /dev/null
+++ b/services/coincap/coincap-priceusd.tester.js
@@ -0,0 +1,29 @@
+import { isCurrency } from '../test-validators.js'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('request for existing asset')
+  .get('/bitcoin.json')
+  .intercept(nock =>
+    nock('https://api.coincap.io')
+      .get('/v2/assets/bitcoin')
+      .reply(200, {
+        data: { priceUsd: '16417.7176754790740415', name: 'Bitcoin' },
+      })
+  )
+  .expectBadge({
+    label: 'bitcoin',
+    message: '$16,417.72',
+    color: 'blue',
+  })
+
+t.create('price usd').get('/bitcoin.json').expectBadge({
+  label: 'bitcoin',
+  message: isCurrency,
+  color: 'blue',
+})
+
+t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({
+  label: 'coincap',
+  message: 'asset not found',
+})
diff --git a/services/coincap/coincap-rank.service.js b/services/coincap/coincap-rank.service.js
new file mode 100644
index 0000000000000000000000000000000000000000..7240b4ee9286a727dc0fa2fa3bababad3ccf6ab4
--- /dev/null
+++ b/services/coincap/coincap-rank.service.js
@@ -0,0 +1,37 @@
+import Joi from 'joi'
+import BaseCoincapService from './coincap-base.js'
+
+const schema = Joi.object({
+  data: Joi.object({
+    rank: Joi.string()
+      .pattern(/^[0-9]+$/)
+      .required(),
+    name: Joi.string().required(),
+  }).required(),
+}).required()
+
+export default class CoincapRank extends BaseCoincapService {
+  static route = { base: 'coincap/rank', pattern: ':assetId' }
+
+  static examples = [
+    {
+      title: 'Coincap (Rank)',
+      namedParams: { assetId: 'bitcoin' },
+      staticPreview: this.render({ asset: { name: 'bitcoin', rank: '1' } }),
+      keywords: ['bitcoin', 'crypto', 'cryptocurrency'],
+    },
+  ]
+
+  static render({ asset }) {
+    return {
+      label: `${asset.name}`.toLowerCase(),
+      message: asset.rank,
+      color: 'blue',
+    }
+  }
+
+  async handle({ assetId }) {
+    const { data: asset } = await this.fetch({ assetId, schema })
+    return this.constructor.render({ asset })
+  }
+}
diff --git a/services/coincap/coincap-rank.tester.js b/services/coincap/coincap-rank.tester.js
new file mode 100644
index 0000000000000000000000000000000000000000..3ee469a19c686c3862a2716ad7366d530bdfafcf
--- /dev/null
+++ b/services/coincap/coincap-rank.tester.js
@@ -0,0 +1,29 @@
+import Joi from 'joi'
+import { createServiceTester } from '../tester.js'
+export const t = await createServiceTester()
+
+t.create('request for existing asset')
+  .get('/bitcoin.json')
+  .intercept(nock =>
+    nock('https://api.coincap.io')
+      .get('/v2/assets/bitcoin')
+      .reply(200, { data: { rank: '1', name: 'Bitcoin' } })
+  )
+  .expectBadge({
+    label: 'bitcoin',
+    message: '1',
+    color: 'blue',
+  })
+
+t.create('rank')
+  .get('/bitcoin.json')
+  .expectBadge({
+    label: 'bitcoin',
+    message: Joi.number().integer().min(1).required(),
+    color: 'blue',
+  })
+
+t.create('asset not found').get('/not-a-valid-asset.json').expectBadge({
+  label: 'coincap',
+  message: 'asset not found',
+})
diff --git a/services/test-validators.js b/services/test-validators.js
index 3983b46ac28655392e2f10d13c5c99689e1013f3..6c52ce8603b77eab04c6634e6d27ff62078b0806 100644
--- a/services/test-validators.js
+++ b/services/test-validators.js
@@ -94,10 +94,14 @@ const isZeroOverTimePeriod = withRegex(
 )
 
 const isIntegerPercentage = withRegex(/^[1-9][0-9]?%|^100%|^0%$/)
+const isIntegerPercentageNegative = withRegex(/^-?[1-9][0-9]?%|^100%|^0%$/)
 const isDecimalPercentage = withRegex(/^[0-9]+\.[0-9]*%$/)
+const isDecimalPercentageNegative = withRegex(/^-?[0-9]+\.[0-9]*%$/)
 const isPercentage = Joi.alternatives().try(
   isIntegerPercentage,
-  isDecimalPercentage
+  isDecimalPercentage,
+  isIntegerPercentageNegative,
+  isDecimalPercentageNegative
 )
 
 const isFileSize = withRegex(
@@ -164,6 +168,16 @@ const isHumanized = Joi.string().regex(
   /[0-9a-z]+ (second|seconds|minute|minutes|hour|hours|day|days|month|months|year|years)/
 )
 
+// $1,530,602.24 // true
+// 1,530,602.24 // true
+// $1,666.24$ // false
+// ,1,666,88, // false
+// 1.6.66,6 // false
+// .1555. // false
+const isCurrency = withRegex(
+  /(?=.*\d)^\$?(([1-9]\d{0,2}(,\d{3})*)|0)?(\.\d{1,2})?$/
+)
+
 export {
   isSemver,
   isVPlusTripleDottedVersion,
@@ -199,4 +213,5 @@ export {
   isOrdinalNumber,
   isOrdinalNumberDaily,
   isHumanized,
+  isCurrency,
 }