diff --git a/examples/config.py b/examples/config.py
index 4b3c769a4149c383540235ed1074071e146ae8a8..c7d83c7365c15d6c0032e605c678435183574ace 100644
--- a/examples/config.py
+++ b/examples/config.py
@@ -3,8 +3,8 @@ Test configuration.
 """
 
 CONFIG = {
-    "domain": "12f8-197-211-61-33.ngrok-free.app",
-    "user": "testuser",
+    "domain": "Ngrok forwarding url without https://",
+    "user": "username",
     "keys_path": "example_keys",
-    "port": 8880
+    "port": 8880 #port number from ngrok
 } 
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index b8ed079bfed22762bcfbcc43515920d1dbeca305..0c06fd5d8b238b859f39c4fe6fe40717ae911d53 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,7 +13,9 @@ backoff==2.2.1
 beautifulsoup4==4.12.3
 bs4==0.0.2
 cachetools==5.5.0
+certifi==2024.8.30
 cffi==1.17.1
+click==8.1.7
 cryptography==43.0.3
 dnspython==2.7.0
 factory_boy==3.3.1
@@ -21,6 +23,7 @@ Faker==30.8.0
 fastapi==0.115.4
 frozenlist==1.4.1
 greenlet==3.1.1
+h11==0.14.0
 idna==3.10
 iniconfig==2.0.0
 Markdown==3.7
@@ -47,5 +50,5 @@ soupsieve==2.6
 SQLAlchemy==2.0.36
 starlette==0.41.2
 typing_extensions==4.12.2
+uvicorn==0.32.1
 yarl==1.15.2
-certifi
\ No newline at end of file
diff --git a/src/pyfed/federation/webfinger.py b/src/pyfed/federation/webfinger.py
new file mode 100644
index 0000000000000000000000000000000000000000..7dcd164d0380f76603388bb53fa7491dfd0b7a56
--- /dev/null
+++ b/src/pyfed/federation/webfinger.py
@@ -0,0 +1,113 @@
+"""
+WebFinger protocol implementation.
+"""
+
+from typing import Dict, Any, Optional
+import aiohttp
+from urllib.parse import quote
+import logging
+
+logger = logging.getLogger(__name__)
+
+class WebFingerClient:
+    """WebFinger client implementation."""
+
+    def __init__(self, verify_ssl: bool = True):
+        self.verify_ssl = verify_ssl
+        self.session = None
+
+    async def initialize(self) -> None:
+        """Initialize client."""
+        ssl = None if self.verify_ssl else False
+        self.session = aiohttp.ClientSession(
+            headers={"Accept": "application/jrd+json, application/json"},
+            connector=aiohttp.TCPConnector(ssl=ssl)
+        )
+
+    async def finger(self, account: str) -> Optional[Dict[str, Any]]:
+        """
+        Perform WebFinger lookup.
+        
+        Args:
+            account: Account to look up (e.g., user@domain.com)
+            
+        Returns:
+            WebFinger response if found
+        """
+        try:
+            if not '@' in account:
+                return None
+
+            # Ensure acct: prefix
+            if not account.startswith('acct:'):
+                account = f"acct:{account}"
+
+            domain = account.split('@')[-1]
+            url = f"https://{domain}/.well-known/webfinger?resource={quote(account)}"
+
+            response = await self.session.get(url)
+            async with response as resp:
+                if resp.status != 200:
+                    return None
+                return await resp.json()
+
+        except Exception as e:
+            logger.error(f"WebFinger lookup failed for {account}: {e}")
+            return None
+
+    async def get_actor_url(self, account: str) -> Optional[str]:
+        """
+        Get actor URL from WebFinger response.
+        
+        Args:
+            account: Account to look up
+            
+        Returns:
+            Actor URL if found
+        """
+        try:
+            data = await self.finger(account)
+            if not data:
+                return None
+
+            for link in data.get('links', []):
+                if (link.get('rel') == 'self' and 
+                    link.get('type') == 'application/activity+json'):
+                    return link.get('href')
+            return None
+
+        except Exception as e:
+            logger.error(f"Failed to get actor URL for {account}: {e}")
+            return None
+
+    async def get_inbox_url(self, account: str) -> Optional[str]:
+        """
+        Get inbox URL for account.
+        
+        Args:
+            account: Account to look up
+            
+        Returns:
+            Inbox URL if found
+        """
+        try:
+            actor_url = await self.get_actor_url(account)
+            logger.debug(f"actor_url: {actor_url}")
+            if not actor_url:
+                return None
+
+            response = await self.session.get(actor_url)
+            async with response as resp:
+                if resp.status != 200:
+                    return None
+                data = await resp.json()
+                return data.get('inbox')
+
+        except Exception as e:
+            logger.error(f"Failed to get inbox URL for {account}: {e}")
+            return None
+
+    async def close(self) -> None:
+        """Clean up resources."""
+        if self.session:
+            await self.session.close() 
\ No newline at end of file