diff --git a/examples/README.md b/examples/README.md
index 39249ed1a2b4edc7e10e4b738b0a899b868a0d3f..95961a26eebd82f87e826882189082fb1ae9a069 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,2 +1,118 @@
-To run examples, use the following command:
-`python <example.py>`
\ No newline at end of file
+# PyFed Mastodon Integration Demo
+
+This guide demonstrates how to use PyFed to interact with Mastodon using the ActivityPub protocol. The example shows how to send a message (Note) to a Mastodon user using federation.
+
+## Prerequisites
+
+1. Python 3.9 or higher
+2. Virtual environment (recommended)
+3. ngrok installed (for local testing)
+4. A Mastodon account to send messages to
+
+## Installation
+
+1. Clone the repository and set up the environment:
+
+```bash
+# Create and activate virtual environment
+python -m venv .venv
+source .venv/bin/activate  # On Windows: .venv\Scripts\activate
+
+# Install dependencies
+pip install -r requirements.txt
+```
+
+2. Set up ngrok for local testing:
+```bash
+# Start ngrok on port 8880
+ngrok http 8880
+```
+Save the ngrok URL (e.g., "12f8-197-211-61-33.ngrok-free.app") for configuration.
+
+## Configuration
+
+1. Create a `config.py` file in the examples directory with your settings:
+
+```python
+CONFIG = {
+    "domain": "12f8-197-211-61-33.ngrok-free.app",  # Your ngrok URL (without https://)
+    "user": "testuser",
+    "keys_path": "example_keys",
+    "port": 8880
+}
+```
+
+2. Create the keys directory:
+```bash
+mkdir example_keys
+```
+
+3. Start the key server:
+```bash
+python examples/key_server.py
+```
+The key server will handle key management and HTTP signatures automatically.
+
+## Running the Example
+
+1. Ensure both ngrok and the key server are running
+
+2. Run the example:
+```bash
+python examples/send_to_mastodon.py
+```
+
+3. Check the logs for the federation process:
+   - WebFinger lookup
+   - Actor discovery
+   - Message delivery
+   - HTTP signatures
+
+## Troubleshooting
+
+Common issues and solutions:
+
+1. **Connection Issues**
+   - Verify ngrok is running and URL is correct
+   - Check if key server is running on port 8880
+   - Ensure ngrok URL is properly set in config
+
+2. **WebFinger Lookup Fails**
+   - Verify the Mastodon username is correct
+   - Check if the instance is online
+   - Ensure proper network connectivity
+
+3. **Delivery Errors**
+   - Verify your ngrok URL is current (they expire)
+   - Check key server logs for signature issues
+   - Ensure Mastodon instance is reachable
+
+## Understanding the Code
+
+The example demonstrates several key PyFed features:
+
+1. **Key Management**
+   - Automatic key generation and management via key server
+   - Handles HTTP signatures for federation
+
+2. **Federation**
+   - WebFinger protocol for user discovery
+   - ActivityPub protocol for message delivery
+
+3. **Activity Creation**
+   - Creates ActivityPub Note objects
+   - Wraps them in Create activities
+
+## Next Steps
+
+1. Try sending different types of activities
+2. Implement inbox handling for responses
+3. Add error handling and retries
+4. Explore other ActivityPub interactions
+
+## Support
+
+For issues and questions:
+1. Check the PyFed documentation
+2. Open an issue on GitHub
+3. Join our community discussions
\ No newline at end of file
diff --git a/src/pyfed/federation/__init__.py b/src/pyfed/federation/__init__.py
index 7770cfdadd2a832700722298a1c75e52ed719950..a298d4341900269b23a1f4dbdc324b135a76b629 100644
--- a/src/pyfed/federation/__init__.py
+++ b/src/pyfed/federation/__init__.py
@@ -6,10 +6,12 @@ from .delivery import ActivityDelivery
 from .fetch import ResourceFetcher
 from .resolver import ActivityPubResolver
 from .discovery import InstanceDiscovery
+from .webfinger import WebFingerClient
 
 __all__ = [
     'ActivityDelivery',
     'ResourceFetcher',
-    'ActivityPubResolver',
-    'InstanceDiscovery'
+    'ActivityPubResolver',  
+    'InstanceDiscovery',
+    'WebFingerClient'
 ] 
\ No newline at end of file
diff --git a/src/pyfed/federation/resolver.py b/src/pyfed/federation/resolver.py
index 6dda1ddcee695c5e191e21c2b52c71ec1e734a11..be2bd63d474ce1fa451caf13062eec81c2e477f5 100644
--- a/src/pyfed/federation/resolver.py
+++ b/src/pyfed/federation/resolver.py
@@ -10,7 +10,7 @@ from urllib.parse import urlparse
 from ..utils.exceptions import ResolverError
 from ..utils.logging import get_logger
 from ..cache.actor_cache import ActorCache
-from ..protocols.webfinger import WebFingerClient
+from .webfinger import WebFingerClient
 
 logger = get_logger(__name__)
 
diff --git a/src/pyfed/models/__init__.py b/src/pyfed/models/__init__.py
index 365021f853072f44e6df0cba3f20b54c06d716e4..205ffe9e0ada3099adaebac266262a75706944a4 100644
--- a/src/pyfed/models/__init__.py
+++ b/src/pyfed/models/__init__.py
@@ -13,7 +13,7 @@ from .collections import (
 )
 from .activities import (
     APCreate, APUpdate, APDelete,
-    APFollow, APUndo, APLike, APAnnounce, APActivity
+    APFollow, APUndo, APLike, APAnnounce, APActivity, APAccept, APRemove, APBlock, APReject
 )
 from .links import APLink, APMention
 
@@ -23,5 +23,5 @@ __all__ = [
     'APActor', 'APPerson', 'APGroup', 'APOrganization', 'APApplication', 'APService',
     'APEvent', 'APPlace', 'APProfile', 'APRelationship', 'APTombstone', 'APArticle', 'APAudio', 'APDocument', 'APImage', 'APNote', 'APPage', 'APVideo',
     'APCollection', 'APOrderedCollection', 'APCollectionPage', 'APOrderedCollectionPage',
-    'APCreate', 'APUpdate', 'APDelete', 'APFollow', 'APUndo', 'APLike', 'APAnnounce', 'APMention'
+    'APCreate', 'APUpdate', 'APDelete', 'APFollow', 'APUndo', 'APLike', 'APAnnounce', 'APMention', 'APAccept', 'APRemove', 'APBlock', 'APReject'
 ]
diff --git a/src/pyfed/models/activities.py b/src/pyfed/models/activities.py
index 50646e21d31c1f7381f51e747b8c92e8ce2a9d26..25e609408a04a0b5624a25b2670476535f70a8b2 100644
--- a/src/pyfed/models/activities.py
+++ b/src/pyfed/models/activities.py
@@ -181,3 +181,76 @@ class APAnnounce(APActivity):
         ...,  
         description="The object being announced"
     )
+
+class APAccept(APActivity):
+    """
+    Represents an Accept activity in ActivityPub.
+
+    Indicates that the actor accepts the object.
+
+    Attributes:
+        type (Literal["Accept"]): Always set to "Accept".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#accept
+    """
+    type: Literal["Accept"] = Field(default="Accept", description="Indicates that this object represents an accept activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The object being accepted"
+    )
+
+class APRemove(APActivity):
+    """
+    Represents a Remove activity in ActivityPub.
+
+    Indicates that the actor is removing the object.
+
+    Attributes:
+        type (Literal["Remove"]): Always set to "Remove".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#remove
+    """
+    type: Literal["Remove"] = Field(default="Remove", description="Indicates that this object represents a remove activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The object being removed"
+    )
+
+class APBlock(APActivity):
+    """
+    Represents a Block activity in ActivityPub.
+
+    Indicates that the actor is blocking the object.
+
+    Attributes:
+        type (Literal["Block"]): Always set to "Block".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#block
+    """
+    type: Literal["Block"] = Field(default="Block", description="Indicates that this object represents a block activity")
+    object: Union[str, HttpUrl, APObject] = Field(
+        ...,  
+        description="The object being blocked"
+    )
+
+class APReject(APActivity):
+    """
+    Represents a Reject activity in ActivityPub.
+
+    Indicates that the actor rejects the object.
+    This is typically used to reject incoming activities such as Follow requests.
+
+    Attributes:
+        type (Literal["Reject"]): Always set to "Reject".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#reject
+    """
+    type: Literal["Reject"] = Field(default="Reject", description="Indicates that this object represents a reject activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The object being rejected"
+    )
diff --git a/src/pyfed/models/collections.py b/src/pyfed/models/collections.py
index 64a9759b43864b3e8f0eb802a28ab3ca1a335a84..aae976127ea66be7e973564b41cfbfe5f2f10a8d 100644
--- a/src/pyfed/models/collections.py
+++ b/src/pyfed/models/collections.py
@@ -9,7 +9,7 @@ https://www.w3.org/TR/activitystreams-core/#collections
 
 from __future__ import annotations
 from typing import Optional, List, Union, Literal, Dict, Any
-from pydantic import Field, HttpUrl
+from pydantic import Field, BaseModel, HttpUrl
 from pyfed.models.objects import APObject
 from pyfed.utils.logging import get_logger
 
@@ -101,6 +101,26 @@ class APOrderedCollectionPage(APCollectionPage):
         description="The index of the first item"
     )
 
+class APFollowersCollection(APOrderedCollection):
+    """Collection of followers."""
+    type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
+    name: Literal["Followers"] = Field(default="Followers")
+
+class APFollowingCollection(APOrderedCollection):
+    """Collection of following."""
+    type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
+    name: Literal["Following"] = Field(default="Following")
+
+class APLikedCollection(APOrderedCollection):
+    """Collection of liked objects."""
+    type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
+    name: Literal["Liked"] = Field(default="Liked")
+
+class APSharedCollection(APOrderedCollection):
+    """Collection of shared objects."""
+    type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
+    name: Literal["Shared"] = Field(default="Shared")
+
 async def fetch_collection(url: str) -> Union[APCollection, APOrderedCollection]:
     """
     Fetch a collection from a given URL.
diff --git a/src/pyfed/protocols/__init__.py b/src/pyfed/protocols/__init__.py
index b6f4736c422d2d586f76fc4e47c24a9a93a4d05c..b0d1be4fe2125376c7e14597c6d14bf42f3244d1 100644
--- a/src/pyfed/protocols/__init__.py
+++ b/src/pyfed/protocols/__init__.py
@@ -1,6 +1,27 @@
-# from .c2s import C2SConfig, ClientToServerProtocol
-# from .s2s import ServerToServerProtocol
+"""
+Protocol implementations for PyFed.
+"""
+from .base import (
+    BaseProtocol, BaseProtocolConfig, ProtocolVersion,
+    ProtocolError, ValidationError, SecurityError, TimeoutError
+)
+from .c2s import C2SConfig, ClientToServerProtocol
+from .s2s import ServerToServerProtocol
 
-# __all__ = [
-#     'C2SConfig', 'ClientToServerProtocol', 'ServerToServerProtocol'
-# ]
+__all__ = [
+    # Base
+    'BaseProtocol',
+    'BaseProtocolConfig',
+    'ProtocolVersion',
+    'ProtocolError',
+    'ValidationError',
+    'SecurityError',
+    'TimeoutError',
+    
+    # Client-to-Server
+    'C2SConfig',
+    'ClientToServerProtocol',
+    
+    # Server-to-Server
+    'ServerToServerProtocol',
+]
diff --git a/src/pyfed/protocols/webfinger.py b/src/pyfed/protocols/webfinger.py
deleted file mode 100644
index 7dcd164d0380f76603388bb53fa7491dfd0b7a56..0000000000000000000000000000000000000000
--- a/src/pyfed/protocols/webfinger.py
+++ /dev/null
@@ -1,113 +0,0 @@
-"""
-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
diff --git a/src/pyfed/requirements.txt b/src/pyfed/requirements.txt
index 0519ecba6ea913e21689ec692e81e9e4973fbf73..b8ed079bfed22762bcfbcc43515920d1dbeca305 100644
--- a/src/pyfed/requirements.txt
+++ b/src/pyfed/requirements.txt
@@ -1 +1,51 @@
- 
\ No newline at end of file
+aiofiles==24.1.0
+aiohappyeyeballs==2.4.3
+aiohttp==3.10.10
+aioredis==2.0.1
+aiosignal==1.3.1
+aiosqlite==0.20.0
+annotated-types==0.7.0
+anyio==4.6.2.post1
+async-timeout==5.0.0
+asyncpg==0.30.0
+attrs==24.2.0
+backoff==2.2.1
+beautifulsoup4==4.12.3
+bs4==0.0.2
+cachetools==5.5.0
+cffi==1.17.1
+cryptography==43.0.3
+dnspython==2.7.0
+factory_boy==3.3.1
+Faker==30.8.0
+fastapi==0.115.4
+frozenlist==1.4.1
+greenlet==3.1.1
+idna==3.10
+iniconfig==2.0.0
+Markdown==3.7
+motor==3.6.0
+multidict==6.1.0
+packaging==24.1
+pluggy==1.5.0
+prometheus_client==0.21.0
+propcache==0.2.0
+pycparser==2.22
+pydantic==2.9.2
+pydantic_core==2.23.4
+PyJWT==2.9.0
+pymongo==4.9.2
+pytest==8.3.3
+pytest-aiohttp==1.0.5
+pytest-asyncio==0.24.0
+python-dateutil==2.9.0.post0
+PyYAML==6.0.2
+redis==5.2.0
+six==1.16.0
+sniffio==1.3.1
+soupsieve==2.6
+SQLAlchemy==2.0.36
+starlette==0.41.2
+typing_extensions==4.12.2
+yarl==1.15.2
+certifi
\ No newline at end of file