diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 4650f03a29..a77991cccd 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -468,6 +468,10 @@ unableToProcess: "操作を完了できません"
 recentUsed: "最近使用"
 install: "インストール"
 uninstall: "アンインストール"
+installedApps: "インストールされたアプリ"
+nothing: "ありません"
+installedDate: "インストール日時"
+lastUsedDate: "最終使用日時"
 
 _theme:
   explore: "テーマを探す"
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index 65dda17575..0cf4dee2dd 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mk-notes" v-size="[{ max: 500 }]">
-	<div class="empty" v-if="empty">
+	<div class="_fullinfo" v-if="empty">
 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 		<div>{{ $t('noNotes') }}</div>
 	</div>
@@ -90,18 +90,6 @@ export default Vue.extend({
 
 <style lang="scss" scoped>
 .mk-notes {
-	> .empty {
-		padding: 32px;
-		text-align: center;
-
-		> img {
-			vertical-align: bottom;
-			height: 128px;
-			margin-bottom: 16px;
-			border-radius: 16px;
-		}
-	}
-
 	> .notes {
 		> ::v-deep *:not(:last-child) {
 			margin-bottom: var(--marginFull);
diff --git a/src/client/pages/apps.vue b/src/client/pages/apps.vue
new file mode 100644
index 0000000000..e25bffa2bc
--- /dev/null
+++ b/src/client/pages/apps.vue
@@ -0,0 +1,98 @@
+<template>
+<div>
+	<portal to="icon"><fa :icon="faPlug"/></portal>
+	<portal to="title">{{ $t('installedApps') }}</portal>
+
+	<mk-pagination :pagination="pagination" class="bfomjevm" ref="list">
+		<template #empty>
+			<div class="_fullinfo">
+				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
+				<div>{{ $t('nothing') }}</div>
+			</div>
+		</template>
+		<template #default="{items}">
+			<div class="token _panel" v-for="token in items" :key="token.id">
+				<img class="icon" :src="token.iconUrl" alt=""/>
+				<div class="body">
+					<div class="name">{{ token.name }}</div>
+					<div class="description">{{ token.description }}</div>
+					<div class="_keyValue">
+						<div>{{ $t('installedDate') }}:</div>
+						<div><mk-time :time="token.createdAt"/></div>
+					</div>
+					<div class="_keyValue">
+						<div>{{ $t('lastUsedDate') }}:</div>
+						<div><mk-time :time="token.lastUsedAt"/></div>
+					</div>
+					<div class="actions">
+						<button class="_button" @click="revoke(token)"><fa :icon="faTrashAlt"/></button>
+					</div>
+				</div>
+			</div>
+		</template>
+	</mk-pagination>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
+import MkPagination from '../components/ui/pagination.vue';
+
+export default Vue.extend({
+	metaInfo() {
+		return {
+			title: this.$t('installedApps') as string
+		};
+	},
+
+	components: {
+		MkPagination
+	},
+
+	data() {
+		return {
+			pagination: {
+				endpoint: 'i/apps',
+				limit: 100,
+			},
+			faTrashAlt, faPlug
+		};
+	},
+
+	methods: {
+		revoke(token) {
+			this.$root.api('i/revoke-token', { tokenId: token.id }).then(() => {
+				this.$refs.list.reload();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.bfomjevm {
+	> .token {
+		display: flex;
+		padding: 16px;
+
+		> .icon {
+			display: block;
+			flex-shrink: 0;
+			margin: 0 12px 0 0;
+			width: 50px;
+			height: 50px;
+			border-radius: 8px;
+		}
+
+		> .body {
+			width: calc(100% - 62px);
+			position: relative;
+
+			> .name {
+				font-weight: bold;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue
index a900bf735c..b310d9f581 100644
--- a/src/client/pages/follow-requests.vue
+++ b/src/client/pages/follow-requests.vue
@@ -5,7 +5,7 @@
 
 	<mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list">
 		<template #empty>
-			<div class="tkdrhpxr">
+			<div class="_fullinfo">
 				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 				<div>{{ $t('noFollowRequests') }}</div>
 			</div>
@@ -75,18 +75,6 @@ export default Vue.extend({
 
 <style lang="scss" scoped>
 .mk-follow-requests {
-	.tkdrhpxr {
-		padding: 32px;
-		text-align: center;
-
-		> img {
-			vertical-align: bottom;
-			height: 128px;
-			margin-bottom: 16px;
-			border-radius: 16px;
-		}
-	}
-
 	> .user {
 		display: flex;
 		padding: 16px;
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
index ed24f8ef54..7a55004cbf 100644
--- a/src/client/pages/messaging/index.vue
+++ b/src/client/pages/messaging/index.vue
@@ -31,7 +31,7 @@
 			</div>
 		</router-link>
 	</div>
-	<div class="no-history" v-if="!fetching && messages.length == 0">
+	<div class="_fullinfo" v-if="!fetching && messages.length == 0">
 		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 		<div>{{ $t('noHistory') }}</div>
 	</div>
@@ -287,18 +287,6 @@ export default Vue.extend({
 		}
 	}
 
-	> .no-history {
-		padding: 32px;
-		text-align: center;
-
-		> img {
-			vertical-align: bottom;
-			height: 128px;
-			margin-bottom: 16px;
-			border-radius: 16px;
-		}
-	}
-
 	@media (max-width: 400px) {
 		> .history {
 			> .message {
diff --git a/src/client/pages/my-settings/index.vue b/src/client/pages/my-settings/index.vue
index 4742793f2b..c3080e0f81 100644
--- a/src/client/pages/my-settings/index.vue
+++ b/src/client/pages/my-settings/index.vue
@@ -32,7 +32,9 @@
 	<x-integration/>
 	<x-api/>
 
-	<mk-button @click="$root.signout()" primary style="margin: var(--margin) auto;">{{ $t('logout') }}</mk-button>
+	<router-link class="_panel _buttonPrimary" to="/my/apps" style="margin: var(--margin) auto;">{{ $t('installedApps') }}</router-link>
+
+	<button class="_panel _buttonPrimary" @click="$root.signout()" style="margin: var(--margin) auto;">{{ $t('logout') }}</button>
 </div>
 </template>
 
diff --git a/src/client/router.ts b/src/client/router.ts
index 70d497d36f..9644ede55f 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -46,6 +46,7 @@ export const router = new VueRouter({
 		{ path: '/my/groups', component: page('my-groups/index') },
 		{ path: '/my/groups/:group', component: page('my-groups/group') },
 		{ path: '/my/antennas', component: page('my-antennas/index') },
+		{ path: '/my/apps', component: page('apps') },
 		{ path: '/preferences', component: page('preferences/index') },
 		{ path: '/instance', component: page('instance/index') },
 		{ path: '/instance/emojis', component: page('instance/emojis') },
diff --git a/src/client/style.scss b/src/client/style.scss
index 7b509e5b51..57906d5ae7 100644
--- a/src/client/style.scss
+++ b/src/client/style.scss
@@ -412,6 +412,26 @@ main ._panel {
 	}
 }
 
+._fullinfo {
+	padding: 32px;
+	text-align: center;
+
+	> img {
+		vertical-align: bottom;
+		height: 128px;
+		margin-bottom: 16px;
+		border-radius: 16px;
+	}
+}
+
+._keyValue {
+	display: flex;
+
+	> div {
+		flex: 1;
+	}
+}
+
 ._link {
 	color: var(--link);
 }
diff --git a/src/server/api/endpoints/i/revoke-token.ts b/src/server/api/endpoints/i/revoke-token.ts
new file mode 100644
index 0000000000..ce688c5755
--- /dev/null
+++ b/src/server/api/endpoints/i/revoke-token.ts
@@ -0,0 +1,24 @@
+import $ from 'cafy';
+import define from '../../define';
+import { AccessTokens } from '../../../../models';
+import { ID } from '../../../../misc/cafy-id';
+
+export const meta = {
+	requireCredential: true as const,
+
+	secure: true,
+
+	params: {
+		tokenId: {
+			validator: $.type(ID)
+		}
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const token = await AccessTokens.findOne(ps.tokenId);
+
+	if (token) {
+		AccessTokens.delete(token.id);
+	}
+});