diff --git a/services/github/github-commit-activity.service.js b/services/github/github-commit-activity.service.js
index ee7ad583ac849bd239a26f1cc7013d6b00a0c3a6..821827a77d051e013a257ef320c3c15fea877d94 100644
--- a/services/github/github-commit-activity.service.js
+++ b/services/github/github-commit-activity.service.js
@@ -1,22 +1,28 @@
+import gql from 'graphql-tag'
 import Joi from 'joi'
+import { InvalidResponse } from '../index.js'
 import { metric } from '../text-formatters.js'
 import { nonNegativeInteger } from '../validators.js'
-import { GithubAuthV3Service } from './github-auth-service.js'
-import { errorMessagesFor, documentation } from './github-helpers.js'
+import { GithubAuthV4Service } from './github-auth-service.js'
+import { transformErrors, documentation } from './github-helpers.js'
 
-const schema = Joi.array()
-  .items(
-    Joi.object({
-      total: nonNegativeInteger,
-    })
-  )
-  .required()
+const schema = Joi.object({
+  data: Joi.object({
+    repository: Joi.object({
+      object: Joi.object({
+        history: Joi.object({
+          totalCount: nonNegativeInteger,
+        }).required(),
+      }),
+    }).required(),
+  }).required(),
+}).required()
 
-export default class GithubCommitActivity extends GithubAuthV3Service {
+export default class GitHubCommitActivity extends GithubAuthV4Service {
   static category = 'activity'
   static route = {
     base: 'github/commit-activity',
-    pattern: ':interval(y|m|4w|w)/:user/:repo',
+    pattern: ':interval(y|m|4w|w)/:user/:repo/:branch*',
   }
 
   static examples = [
@@ -29,6 +35,20 @@ export default class GithubCommitActivity extends GithubAuthV3Service {
       keywords: ['commits'],
       documentation,
     },
+    {
+      title: 'GitHub commit activity (branch)',
+      // Override the pattern to omit the deprecated interval "4w".
+      pattern: ':interval(y|m|w)/:user/:repo/:branch*',
+      namedParams: {
+        interval: 'm',
+        user: 'badges',
+        repo: 'squint',
+        branch: 'main',
+      },
+      staticPreview: this.render({ interval: 'm', commitCount: 5 }),
+      keywords: ['commits'],
+      documentation,
+    },
   ]
 
   static defaultBadgeData = { label: 'commit activity', color: 'blue' }
@@ -46,47 +66,67 @@ export default class GithubCommitActivity extends GithubAuthV3Service {
     }
   }
 
-  static transform({ interval, weekData }) {
-    const weekTotals = weekData.map(({ total }) => total)
+  async fetch({ interval, user, repo, branch = 'HEAD' }) {
+    const since = this.constructor.getIntervalQueryStartDate({ interval })
+    return this._requestGraphql({
+      query: gql`
+        query (
+          $user: String!
+          $repo: String!
+          $branch: String!
+          $since: GitTimestamp!
+        ) {
+          repository(owner: $user, name: $repo) {
+            object(expression: $branch) {
+              ... on Commit {
+                history(since: $since) {
+                  totalCount
+                }
+              }
+            }
+          }
+        }
+      `,
+      variables: {
+        user,
+        repo,
+        branch,
+        since,
+      },
+      schema,
+      transformErrors,
+    })
+  }
+
+  static transform({ data }) {
+    const {
+      repository: { object: repo },
+    } = data
 
-    if (interval === 'm') {
-      // To approximate the value for the past month, get the sum for the last
-      // four weeks and add a weighted value for the fifth week.
-      const fourWeeksValue = weekTotals
-        .slice(-4)
-        .reduce((sum, weekTotal) => sum + weekTotal, 0)
-      const fifthWeekValue = weekTotals.slice(-5)[0]
-      const averageWeeksPerMonth = 365 / 12 / 7
-      return (
-        fourWeeksValue + Math.round((averageWeeksPerMonth - 4) * fifthWeekValue)
-      )
+    if (!repo) {
+      throw new InvalidResponse({ prettyMessage: 'invalid branch' })
     }
 
-    let wantedWeekData
-    switch (interval) {
-      case 'y':
-        wantedWeekData = weekTotals
-        break
-      case '4w':
-        wantedWeekData = weekTotals.slice(-4)
-        break
-      case 'w':
-        wantedWeekData = weekTotals.slice(-2, -1)
-        break
-      default:
-        throw Error('Unhandled case')
+    return repo.history.totalCount
+  }
+
+  static getIntervalQueryStartDate({ interval }) {
+    const now = new Date()
+
+    if (interval === 'y') {
+      now.setUTCFullYear(now.getUTCFullYear() - 1)
+    } else if (interval === 'm' || interval === '4w') {
+      now.setUTCDate(now.getUTCDate() - 30)
+    } else {
+      now.setUTCDate(now.getUTCDate() - 7)
     }
 
-    return wantedWeekData.reduce((sum, weekTotal) => sum + weekTotal, 0)
+    return now.toISOString()
   }
 
-  async handle({ interval, user, repo }) {
-    const weekData = await this._requestJson({
-      url: `/repos/${user}/${repo}/stats/commit_activity`,
-      schema,
-      errorMessages: errorMessagesFor(),
-    })
-    const commitCount = this.constructor.transform({ interval, weekData })
+  async handle({ interval, user, repo, branch }) {
+    const json = await this.fetch({ interval, user, repo, branch })
+    const commitCount = this.constructor.transform(json)
     return this.constructor.render({ interval, commitCount })
   }
 }
diff --git a/services/github/github-commit-activity.spec.js b/services/github/github-commit-activity.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..1815bf2dd90c1146025c4573abcee0f7de9b0de8
--- /dev/null
+++ b/services/github/github-commit-activity.spec.js
@@ -0,0 +1,64 @@
+import { expect } from 'chai'
+import sinon from 'sinon'
+import { InvalidResponse } from '../index.js'
+import GitHubCommitActivity from './github-commit-activity.service.js'
+
+describe('GitHubCommitActivity', function () {
+  describe('transform', function () {
+    it('throws InvalidResponse on invalid branch and null object', function () {
+      expect(() =>
+        GitHubCommitActivity.transform({
+          data: { repository: { object: null } },
+        })
+      )
+        .to.throw(InvalidResponse)
+        .with.property('prettyMessage', 'invalid branch')
+    })
+  })
+  describe('getIntervalQueryStartDate', function () {
+    /** @type {sinon.SinonFakeTimers} */
+    let clock
+    beforeEach(function () {
+      clock = sinon.useFakeTimers()
+    })
+    afterEach(function () {
+      clock.restore()
+    })
+
+    it('provides correct value for yearly interval', function () {
+      clock.tick(new Date('2021-08-28T02:21:34.000Z').getTime())
+      expect(
+        GitHubCommitActivity.getIntervalQueryStartDate({
+          interval: 'y',
+        })
+      ).to.equal('2020-08-28T02:21:34.000Z')
+    })
+
+    it('provides correct value for simple monthly interval', function () {
+      clock.tick(new Date('2021-03-31T02:21:34.000Z').getTime())
+      expect(
+        GitHubCommitActivity.getIntervalQueryStartDate({
+          interval: 'm',
+        })
+      ).to.equal('2021-03-01T02:21:34.000Z')
+    })
+
+    it('provides correct value for fun monthly interval', function () {
+      clock.tick(new Date('2021-03-07T02:21:34.000Z').getTime())
+      expect(
+        GitHubCommitActivity.getIntervalQueryStartDate({
+          interval: '4w',
+        })
+      ).to.equal('2021-02-05T02:21:34.000Z')
+    })
+
+    it('provides correct value for weekly interval', function () {
+      clock.tick(new Date('2021-12-31T23:59:34.000Z').getTime())
+      expect(
+        GitHubCommitActivity.getIntervalQueryStartDate({
+          interval: 'w',
+        })
+      ).to.equal('2021-12-24T23:59:34.000Z')
+    })
+  })
+})
diff --git a/services/github/github-commit-activity.tester.js b/services/github/github-commit-activity.tester.js
index bd18f2d68821ea44a69d65378caae9f73668656c..b109d5b8102bac3452438bd0ecd9db2aab49ef65 100644
--- a/services/github/github-commit-activity.tester.js
+++ b/services/github/github-commit-activity.tester.js
@@ -33,6 +33,13 @@ t.create('commit activity (1 week)').get('/w/eslint/eslint.json').expectBadge({
   message: isCommitActivity,
 })
 
+t.create('commit activity (custom branch)')
+  .get('/y/badges/squint/main.json')
+  .expectBadge({
+    label: 'commit activity',
+    message: isCommitActivity,
+  })
+
 t.create('commit activity (repo not found)')
   .get('/w/badges/helmets.json')
   .expectBadge({