diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000000000000000000000000000000000..815554c74f1daf7291db3bbda4d6f557d5d8c693
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+language: python
+
+python:
+  - "2.6"
+  - "2.7"
+
+install: make
+
+script: make test
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..dd37a921b8c11e8a29e5c4460d02947c3cf08e43
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,43 @@
+all: install_requirements develop todo
+
+develop:
+	python setup.py develop
+
+install_requirements:
+	pip install -r requirements.txt --use-mirrors
+
+install:
+	python setup.py install
+
+uninstall:
+	pip uninstall -y aprs
+
+todo:
+	grep \#\ TODO Makefile
+
+clean:
+	rm -rf *.egg* build dist *.py[oc] */*.py[co] cover doctest_pypi.cfg \
+	 	nosetests.xml pylint.log *.egg output.xml flake8.log tests.log \
+		test-result.xml htmlcov fab.log
+
+publish:
+	python setup.py register sdist upload
+
+nosetests:
+	python setup.py nosetests
+
+pep8:
+	flake8
+
+flake8:
+	flake8 --exit-zero  --max-complexity 12 aprs/*.py tests/*.py *.py | \
+		awk -F\: '{printf "%s:%s: [E]%s\n", $$1, $$2, $$3}' | tee flake8.log
+
+clonedigger:
+	clonedigger --cpd-output .
+
+lint:
+	pylint -f parseable -i y -r y aprs/*.py tests/*.py *.py | \
+		tee pylint.log
+
+test: install_requirements lint clonedigger flake8 nosetests
diff --git a/aprs/__init__.py b/aprs/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9897db3e8fc0d70b4fdd8876153ba6222af8c6bd
--- /dev/null
+++ b/aprs/__init__.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+
+__author__ = 'Greg Albrecht <gba@gregalbrecht.com>'
+__copyright__ = 'Copyright 2013 Greg Albrecht'
+__license__ = 'Creative Commons Attribution 3.0 Unported License'
+
+
+from .aprs import APRS
diff --git a/aprs/aprs.py b/aprs/aprs.py
new file mode 100755
index 0000000000000000000000000000000000000000..8ffefc6436e9101509285669e76ca2e4b889585c
--- /dev/null
+++ b/aprs/aprs.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+
+
+import logging
+import logging.handlers
+
+import requests
+
+
+class APRS(object):
+
+    logger = logging.getLogger('aprs')
+    logger.addHandler(logging.StreamHandler())
+
+    def __init__(self, user, password=-1, input_url=None):
+        self._url = input_url or 'http://srvr.aprs-is.net:8080'
+        self._auth = "user %s pass %s" % (user, password)
+
+    def send(self, message):
+        headers = {
+            'content-type': 'application/octet-stream',
+            'accept': 'text/plain'
+        }
+
+        content = "\n".join([self._auth, message])
+
+        result = requests.post(self._url, data=content, headers=headers)
+
+        return result.status_code == 204
diff --git a/setup.py b/setup.py
index 2d8ebb63d400a7c4f095108745366f8ed971cc3d..19617844cade9b8e3e5d75d93858a9fe3a9781b7 100644
--- a/setup.py
+++ b/setup.py
@@ -16,5 +16,8 @@ setuptools.setup(
     author_email='gba@gregalbrecht.com',
     license='Creative Commons Attribution 3.0 Unported License',
     url='https://github.com/ampledata/aprs',
-    setup_requires=['nose>=1.0']
+    setup_requires=['nose'],
+    tests_require=['nose', 'httpretty'],
+    install_requires=['requests']
+
 )
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/context.py b/tests/context.py
new file mode 100644
index 0000000000000000000000000000000000000000..922205b5f6c18632f645ca3198a2cf5802c14f04
--- /dev/null
+++ b/tests/context.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+"""Test context.
+
+Based on http://kennethreitz.com/repository-structure-and-python.html
+"""
+
+__author__ = 'Greg Albrecht <gba@gregalbrecht.com>'
+__copyright__ = 'Copyright 2013 Greg Albrecht'
+__license__ = 'Creative Commons Attribution 3.0 Unported License'
+
+
+import os
+import sys
+
+
+sys.path.insert(0, os.path.abspath('..'))
+
+
+import aprs
diff --git a/tests/test_aprs.py b/tests/test_aprs.py
new file mode 100644
index 0000000000000000000000000000000000000000..181dcc6d2d4dfc0c099d8356d328962c2459ff51
--- /dev/null
+++ b/tests/test_aprs.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+
+
+import random
+import unittest
+import logging
+import logging.handlers
+
+import httpretty
+
+from .context import aprs
+
+
+ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+NUMBERS = '0123456789'
+ALPHANUM = ''.join([ALPHABET, NUMBERS])
+
+
+class APRSTest(unittest.TestCase):
+    """Tests for Python APRS Bindings."""
+
+    logger = logging.getLogger('aprs.tests')
+    logger.addHandler(logging.StreamHandler())
+
+    def random(self, length=8, alphabet=ALPHANUM):
+        return ''.join(random.choice(alphabet) for _ in xrange(length))
+
+    def setUp(self):
+        self.fake_server = ''.join([
+            'http://localhost:',
+            self.random(4, '123456789'),
+            '/'
+        ])
+
+        self.fake_callsign = ''.join([
+            self.random(1, 'KWN'),
+            self.random(1, NUMBERS),
+            self.random(3, ALPHABET),
+            '-',
+            self.random(1, '123456789')
+        ])
+
+        self.real_server = 'http://localhost:14580'
+        self.real_callsign = '-'.join(['W2GMD', self.random(1, '123456789')])
+
+        self.logger.debug("fake_server=%s fake_callsign=%s"
+                          % (self.fake_server, self.fake_callsign))
+
+    @httpretty.httprettified
+    def test_fake_good_auth(self):
+        httpretty.HTTPretty.register_uri(
+            httpretty.HTTPretty.POST,
+            self.fake_server,
+            status=204
+        )
+
+        aprs_conn = aprs.APRS(
+            user=self.fake_callsign,
+            input_url=self.fake_server
+        )
+
+        msg = '>'.join([
+            self.fake_callsign,
+            'APRS,TCPIP*:=3745.00N/12227.00W-Simulated Location'
+        ])
+        self.logger.debug(locals())
+
+        result = aprs_conn.send(msg)
+
+        self.assertTrue(result)
+
+    @httpretty.httprettified
+    def test_fake_bad_auth(self):
+        httpretty.HTTPretty.register_uri(
+            httpretty.HTTPretty.POST,
+            self.fake_server,
+            status=401
+        )
+
+        aprs_conn = aprs.APRS(
+            user=self.fake_callsign,
+            input_url=self.fake_server
+        )
+
+        msg = '>'.join([
+            self.fake_callsign,
+            'APRS,TCPIP*:=3745.00N/12227.00W-Simulated Location'
+        ])
+        self.logger.debug(locals())
+
+        result = aprs_conn.send(msg)
+
+        self.assertFalse(result)
+
+    def test_more(self):
+        aprs_conn = aprs.APRS(
+            user=self.real_callsign,
+            input_url=self.real_server
+        )
+
+        msg = '>'.join([
+            self.real_callsign,
+            'APRS,TCPIP*:=3745.00N/12227.00W-Simulated Location'
+        ])
+        self.logger.debug(locals())
+
+        result = aprs_conn.send(msg)
+
+        self.assertFalse(result)