diff --git a/locales/index.d.ts b/locales/index.d.ts
index f0dead1245..dab8eb0361 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5166,6 +5166,10 @@ export interface Locale extends ILocale {
      * 対象
      */
     "target": string;
+    /**
+     * CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
+     */
+    "testCaptchaWarning": string;
     "_abuseUserReport": {
         /**
          * 転送
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 48a670ce50..440ffa9306 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1287,6 +1287,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
 passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
 messageToFollower: "フォロワーへのメッセージ"
 target: "対象"
+testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
 
 _abuseUserReport:
   forward: "転送"
diff --git a/packages/backend/migration/1728550878802-testcaptcha.js b/packages/backend/migration/1728550878802-testcaptcha.js
new file mode 100644
index 0000000000..d8d987c0c1
--- /dev/null
+++ b/packages/backend/migration/1728550878802-testcaptcha.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class Testcaptcha1728550878802 {
+    name = 'Testcaptcha1728550878802'
+
+    async up(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
+    }
+
+    async down(queryRunner) {
+			await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
+    }
+}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index f6b7955cd2..206d0dbe0a 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -119,5 +119,18 @@ export class CaptchaService {
 			throw new Error(`turnstile-failed: ${errorCodes}`);
 		}
 	}
+
+	@bindThis
+	public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
+		if (response == null) {
+			throw new Error('testcaptcha-failed: no response provided');
+		}
+
+		const success = response === 'testcaptcha-passed';
+
+		if (!success) {
+			throw new Error('testcaptcha-failed');
+		}
+	}
 }
 
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index fbd982eb34..409dca3426 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -96,6 +96,7 @@ export class MetaEntityService {
 			recaptchaSiteKey: instance.recaptchaSiteKey,
 			enableTurnstile: instance.enableTurnstile,
 			turnstileSiteKey: instance.turnstileSiteKey,
+			enableTestcaptcha: instance.enableTestcaptcha,
 			swPublickey: instance.swPublicKey,
 			themeColor: instance.themeColor,
 			mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index d29689f907..fd007de6c6 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -258,6 +258,11 @@ export class MiMeta {
 	})
 	public turnstileSecretKey: string | null;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public enableTestcaptcha: boolean;
+
 	// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
 
 	@Column('enum', {
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 99feeaa7d7..e3fd63464a 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -115,6 +115,10 @@ export const packedMetaLiteSchema = {
 			type: 'string',
 			optional: false, nullable: true,
 		},
+		enableTestcaptcha: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
 		swPublickey: {
 			type: 'string',
 			optional: false, nullable: true,
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index be63635efe..3a8cb19f01 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -119,6 +119,7 @@ export class ApiServerService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			}
 		}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
 
@@ -132,6 +133,7 @@ export class ApiServerService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			};
 		}>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
 
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 0d24ffa56a..1d983ca4bc 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -71,6 +71,7 @@ export class SigninApiService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			};
 		}>,
 		reply: FastifyReply,
@@ -194,6 +195,12 @@ export class SigninApiService {
 						throw new FastifyReplyError(400, err);
 					});
 				}
+
+				if (this.meta.enableTestcaptcha) {
+					await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+						throw new FastifyReplyError(400, err);
+					});
+				}
 			}
 
 			if (same) {
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index c499638018..3ec5e5d3e6 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -67,6 +67,7 @@ export class SignupApiService {
 				'g-recaptcha-response'?: string;
 				'turnstile-response'?: string;
 				'm-captcha-response'?: string;
+				'testcaptcha-response'?: string;
 			}
 		}>,
 		reply: FastifyReply,
@@ -99,6 +100,12 @@ export class SignupApiService {
 					throw new FastifyReplyError(400, err);
 				});
 			}
+
+			if (this.meta.enableTestcaptcha) {
+				await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+					throw new FastifyReplyError(400, err);
+				});
+			}
 		}
 
 		const username = body['username'];
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index b76ed5c524..abb3c17be3 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -69,6 +69,10 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: true,
 			},
+			enableTestcaptcha: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
 			swPublickey: {
 				type: 'string',
 				optional: false, nullable: true,
@@ -555,6 +559,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				recaptchaSiteKey: instance.recaptchaSiteKey,
 				enableTurnstile: instance.enableTurnstile,
 				turnstileSiteKey: instance.turnstileSiteKey,
+				enableTestcaptcha: instance.enableTestcaptcha,
 				swPublickey: instance.swPublicKey,
 				themeColor: instance.themeColor,
 				mascotImageUrl: instance.mascotImageUrl,
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 9ffae840b6..e97ac4e2b9 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -78,6 +78,7 @@ export const paramDef = {
 		enableTurnstile: { type: 'boolean' },
 		turnstileSiteKey: { type: 'string', nullable: true },
 		turnstileSecretKey: { type: 'string', nullable: true },
+		enableTestcaptcha: { type: 'boolean' },
 		sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
 		sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
 		setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -357,6 +358,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.turnstileSecretKey = ps.turnstileSecretKey;
 			}
 
+			if (ps.enableTestcaptcha !== undefined) {
+				set.enableTestcaptcha = ps.enableTestcaptcha;
+			}
+
 			if (ps.sensitiveMediaDetection !== undefined) {
 				set.sensitiveMediaDetection = ps.sensitiveMediaDetection;
 			}
diff --git a/packages/frontend/assets/testcaptcha.png b/packages/frontend/assets/testcaptcha.png
new file mode 100644
index 0000000000..9bfd252b51
Binary files /dev/null and b/packages/frontend/assets/testcaptcha.png differ
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index c5b6e0caed..82fc89e51c 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
 		<div ref="captchaEl"></div>
 	</div>
+	<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
+		<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
+		<div v-if="testcaptchaPassed">
+			<div style="color: green;">Test captcha passed!</div>
+		</div>
+		<div v-else>
+			<div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
+			<input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
+			<button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
+		</div>
+	</div>
 	<div v-else ref="captchaEl"></div>
 </div>
 </template>
@@ -29,7 +40,7 @@ export type Captcha = {
 	getResponse(id: string): string;
 };
 
-export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha';
+export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha';
 
 type CaptchaContainer = {
 	readonly [_ in CaptchaProvider]?: Captcha;
@@ -54,12 +65,16 @@ const available = ref(false);
 
 const captchaEl = shallowRef<HTMLDivElement | undefined>();
 
+const testcaptchaInput = ref('');
+const testcaptchaPassed = ref(false);
+
 const variable = computed(() => {
 	switch (props.provider) {
 		case 'hcaptcha': return 'hcaptcha';
 		case 'recaptcha': return 'grecaptcha';
 		case 'turnstile': return 'turnstile';
 		case 'mcaptcha': return 'mcaptcha';
+		case 'testcaptcha': return 'testcaptcha';
 	}
 });
 
@@ -71,6 +86,7 @@ const src = computed(() => {
 		case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit';
 		case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
 		case 'mcaptcha': return null;
+		case 'testcaptcha': return null;
 	}
 });
 
@@ -78,7 +94,7 @@ const scriptId = computed(() => `script-${props.provider}`);
 
 const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
 
-if (loaded || props.provider === 'mcaptcha') {
+if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
 	available.value = true;
 } else if (src.value !== null) {
 	(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
@@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') {
 
 function reset() {
 	if (captcha.value.reset) captcha.value.reset();
+	testcaptchaPassed.value = false;
+	testcaptchaInput.value = '';
 }
 
 async function requestRender() {
@@ -127,6 +145,12 @@ function onReceivedMessage(message: MessageEvent) {
 	}
 }
 
+function testcaptchaSubmit() {
+	testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
+	callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
+	if (!testcaptchaPassed.value) testcaptchaInput.value = '';
+}
+
 onMounted(() => {
 	if (available.value) {
 		window.addEventListener('message', onReceivedMessage);
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
index f30bf5f861..5608122a39 100644
--- a/packages/frontend/src/components/MkSignin.password.vue
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
 				<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 				<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+				<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
 			</div>
 
 			<MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -44,6 +45,7 @@ export type PwResponse = {
 		mCaptchaResponse: string | null;
 		reCaptchaResponse: string | null;
 		turnstileResponse: string | null;
+		testcaptchaResponse: string | null;
 	};
 };
 </script>
@@ -75,18 +77,21 @@ const hCaptcha = useTemplateRef('hcaptcha');
 const mCaptcha = useTemplateRef('mcaptcha');
 const reCaptcha = useTemplateRef('recaptcha');
 const turnstile = useTemplateRef('turnstile');
+const testcaptcha = useTemplateRef('testcaptcha');
 
 const hCaptchaResponse = ref<string | null>(null);
 const mCaptchaResponse = ref<string | null>(null);
 const reCaptchaResponse = ref<string | null>(null);
 const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
 
 const captchaFailed = computed((): boolean => {
 	return (
 		(instance.enableHcaptcha && !hCaptchaResponse.value) ||
 		(instance.enableMcaptcha && !mCaptchaResponse.value) ||
 		(instance.enableRecaptcha && !reCaptchaResponse.value) ||
-		(instance.enableTurnstile && !turnstileResponse.value)
+		(instance.enableTurnstile && !turnstileResponse.value) ||
+		(instance.enableTestcaptcha && !testcaptchaResponse.value)
 	);
 });
 
@@ -104,6 +109,7 @@ function onSubmit() {
 			mCaptchaResponse: mCaptchaResponse.value,
 			reCaptchaResponse: reCaptchaResponse.value,
 			turnstileResponse: turnstileResponse.value,
+			testcaptchaResponse: testcaptchaResponse.value,
 		},
 	});
 }
@@ -113,6 +119,7 @@ function resetCaptcha() {
 	mCaptcha.value?.reset();
 	reCaptcha.value?.reset();
 	turnstile.value?.reset();
+	testcaptcha.value?.reset();
 }
 
 defineExpose({
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index a773cefdab..776ee20e36 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -68,6 +68,8 @@ import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue'
 import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
 
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 import { login } from '@/account.js';
@@ -79,9 +81,6 @@ import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
 import XTotp from '@/components/MkSignin.totp.vue';
 import XPasskey from '@/components/MkSignin.passkey.vue';
 
-import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-
 const emit = defineEmits<{
 	(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
 }>();
@@ -188,6 +187,7 @@ async function onPasswordSubmitted(pw: PwResponse) {
 			'm-captcha-response': pw.captcha.mCaptchaResponse,
 			'g-recaptcha-response': pw.captcha.reCaptchaResponse,
 			'turnstile-response': pw.captcha.turnstileResponse,
+			'testcaptcha-response': pw.captcha.testcaptchaResponse,
 		});
 	}
 }
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index ffb5551ff3..3d1c44fc90 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -66,6 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
 			<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
 			<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+			<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
 			<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
 				<template v-if="submitting">
 					<MkLoading :em="true" :colored="false"/>
@@ -108,6 +109,7 @@ const hcaptcha = ref<Captcha | undefined>();
 const mcaptcha = ref<Captcha | undefined>();
 const recaptcha = ref<Captcha | undefined>();
 const turnstile = ref<Captcha | undefined>();
+const testcaptcha = ref<Captcha | undefined>();
 
 const username = ref<string>('');
 const password = ref<string>('');
@@ -123,6 +125,7 @@ const hCaptchaResponse = ref<string | null>(null);
 const mCaptchaResponse = ref<string | null>(null);
 const reCaptchaResponse = ref<string | null>(null);
 const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
 const usernameAbortController = ref<null | AbortController>(null);
 const emailAbortController = ref<null | AbortController>(null);
 
@@ -132,6 +135,7 @@ const shouldDisableSubmitting = computed((): boolean => {
 		instance.enableMcaptcha && !mCaptchaResponse.value ||
 		instance.enableRecaptcha && !reCaptchaResponse.value ||
 		instance.enableTurnstile && !turnstileResponse.value ||
+		instance.enableTestcaptcha && !testcaptchaResponse.value ||
 		instance.emailRequiredForSignup && emailState.value !== 'ok' ||
 		usernameState.value !== 'ok' ||
 		passwordRetypeState.value !== 'match';
@@ -259,6 +263,7 @@ async function onSubmit(): Promise<void> {
 		'm-captcha-response': mCaptchaResponse.value,
 		'g-recaptcha-response': reCaptchaResponse.value,
 		'turnstile-response': turnstileResponse.value,
+		'testcaptcha-response': testcaptchaResponse.value,
 	};
 
 	const res = await fetch(`${config.apiUrl}/signup`, {
@@ -301,6 +306,7 @@ function onSignupApiError() {
 	mcaptcha.value?.reset?.();
 	recaptcha.value?.reset?.();
 	turnstile.value?.reset?.();
+	testcaptcha.value?.reset?.();
 
 	os.alert({
 		type: 'error',
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index b34592cd6a..d07add4408 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
 	<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
 	<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
+	<template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
 	<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
 	<template v-if="botProtectionForm.modified.value" #footer>
 		<MkFormFooter :form="botProtectionForm"/>
@@ -23,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<option value="mcaptcha">mCaptcha</option>
 			<option value="recaptcha">reCAPTCHA</option>
 			<option value="turnstile">Turnstile</option>
+			<option value="testcaptcha">testCaptcha</option>
 		</MkRadios>
 
 		<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
@@ -85,6 +87,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/>
 			</FormSlot>
 		</template>
+		<template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
+			<MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
+			<FormSlot>
+				<template #label>{{ i18n.ts.preview }}</template>
+				<MkCaptcha provider="testcaptcha"/>
+			</FormSlot>
+		</template>
 	</div>
 </MkFolder>
 </template>
@@ -101,6 +110,7 @@ import { i18n } from '@/i18n.js';
 import { useForm } from '@/scripts/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkFolder from '@/components/MkFolder.vue';
+import MkInfo from '@/components/MkInfo.vue';
 
 const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
 
@@ -115,7 +125,9 @@ const botProtectionForm = useForm({
 				? 'turnstile'
 				: meta.enableMcaptcha
 					? 'mcaptcha'
-					: null,
+					: meta.enableTestcaptcha
+						? 'testcaptcha'
+						: null,
 	hcaptchaSiteKey: meta.hcaptchaSiteKey,
 	hcaptchaSecretKey: meta.hcaptchaSecretKey,
 	mcaptchaSiteKey: meta.mcaptchaSiteKey,
@@ -140,6 +152,7 @@ const botProtectionForm = useForm({
 		enableTurnstile: state.provider === 'turnstile',
 		turnstileSiteKey: state.turnstileSiteKey,
 		turnstileSecretKey: state.turnstileSecretKey,
+		enableTestcaptcha: state.provider === 'testcaptcha',
 	});
 	fetchInstance(true);
 });
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 76ef7ea1fb..e40cb050fd 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4972,6 +4972,7 @@ export type components = {
       recaptchaSiteKey: string | null;
       enableTurnstile: boolean;
       turnstileSiteKey: string | null;
+      enableTestcaptcha: boolean;
       swPublickey: string | null;
       /** @default /assets/ai.png */
       mascotImageUrl: string;
@@ -5102,6 +5103,7 @@ export type operations = {
             recaptchaSiteKey: string | null;
             enableTurnstile: boolean;
             turnstileSiteKey: string | null;
+            enableTestcaptcha: boolean;
             swPublickey: string | null;
             /** @default /assets/ai.png */
             mascotImageUrl: string | null;
@@ -9491,6 +9493,7 @@ export type operations = {
           enableTurnstile?: boolean;
           turnstileSiteKey?: string | null;
           turnstileSecretKey?: string | null;
+          enableTestcaptcha?: boolean;
           /** @enum {string} */
           sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
           /** @enum {string} */