diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts
index 28ac2f968..48efef298 100644
--- a/src/crypto_key.d.ts
+++ b/src/crypto_key.d.ts
@@ -1 +1,2 @@
+export function extractPublic(keypair: String): String;
 export function generate(): String;
diff --git a/src/models/user.ts b/src/models/user.ts
index 4728682d6..02e6a570b 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -278,61 +278,6 @@ export const pack = (
 	resolve(_user);
 });
 
-/**
- * Pack a user for ActivityPub
- *
- * @param user target
- * @return Packed user
- */
-export const packForAp = (
-	user: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
-	let _user: any;
-
-	const fields = {
-		// something
-	};
-
-	// Populate the user if 'user' is ID
-	if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
-		_user = await User.findOne({
-			_id: user
-		}, { fields });
-	} else if (typeof user === 'string') {
-		_user = await User.findOne({
-			_id: new mongo.ObjectID(user)
-		}, { fields });
-	} else {
-		_user = deepcopy(user);
-	}
-
-	if (!_user) return reject('invalid user arg.');
-
-	const userUrl = `${config.url}/@@${_user._id}`;
-
-	resolve({
-		"@context": ["https://www.w3.org/ns/activitystreams", {
-			"@language": "ja"
-		}],
-		"type": "Person",
-		"id": userUrl,
-		"following": `${userUrl}/following.json`,
-		"followers": `${userUrl}/followers.json`,
-		"liked": `${userUrl}/liked.json`,
-		"inbox": `${userUrl}/inbox.json`,
-		"outbox": `${userUrl}/outbox.json`,
-		"sharedInbox": `${config.url}/inbox`,
-		"url": `${config.url}/@${_user.username}`,
-		"preferredUsername": _user.username,
-		"name": _user.name,
-		"summary": _user.description,
-		"icon": [
-			`${config.drive_url}/${_user.avatarId}`
-		]
-	});
-});
-
 /*
 function img(url) {
 	return {
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
new file mode 100644
index 000000000..6cc31de7b
--- /dev/null
+++ b/src/server/activitypub.ts
@@ -0,0 +1,60 @@
+import config from '../conf';
+import { extractPublic } from '../crypto_key';
+import parseAcct from '../common/user/parse-acct';
+import User, { ILocalAccount } from '../models/user';
+const express = require('express');
+
+const app = express();
+
+app.get('/@:user', async (req, res, next) => {
+	const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
+	if (!['application/activity+json', 'application/ld+json'].includes(accepted)) {
+		return next();
+	}
+
+	const { username, host } = parseAcct(req.params.user);
+	if (host !== null) {
+		return res.send(422);
+	}
+
+	const user = await User.findOne({
+		usernameLower: username.toLowerCase(),
+		host: null
+	});
+	if (user === null) {
+		return res.send(404);
+	}
+
+	const id = `${config.url}/@${user.username}`;
+
+	if (username !== user.username) {
+		return res.redirect(id);
+	}
+
+	res.json({
+		'@context': [
+			'https://www.w3.org/ns/activitystreams',
+			'https://w3id.org/security/v1'
+		],
+		type: 'Person',
+		id,
+		preferredUsername: user.username,
+		name: user.name,
+		summary: user.description,
+		icon: user.avatarId && {
+			type: 'Image',
+			url: `${config.drive_url}/${user.avatarId}`
+		},
+		image: user.bannerId && {
+			type: 'Image',
+			url: `${config.drive_url}/${user.bannerId}`
+		},
+		publicKey: {
+			type: 'Key',
+			owner: id,
+			publicKeyPem: extractPublic((user.account as ILocalAccount).keypair)
+		}
+	});
+});
+
+export default app;
diff --git a/src/server/index.ts b/src/server/index.ts
index fe22d9c9b..92d46d46a 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -9,6 +9,7 @@ import * as express from 'express';
 import * as morgan from 'morgan';
 import Accesses from 'accesses';
 
+import activityPub from './activitypub';
 import log from './log-request';
 import config from '../conf';
 
@@ -53,6 +54,7 @@ app.use((req, res, next) => {
  */
 app.use('/api', require('./api'));
 app.use('/files', require('./file'));
+app.use(activityPub);
 app.use(require('./web'));
 
 function createServer() {