diff --git a/Changelog.md b/Changelog.md
index ea308860cdd5b2d515dc62d5716e4a45463533a6..32c39090bfc7425b3288c99c6adba55b2a7f2e67 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -2,6 +2,10 @@
 
 All notable changes to this project will be documented in this file.
 
+## [0.3] - 2021-08-20
+- less strict inventory response validation
+- gws direct & indirect checks
+
 ## [0.2] - 2021-06-10
 - ensure checks are published
 
diff --git a/brian_polling_manager/configuration.py b/brian_polling_manager/configuration.py
index bbb136dd4601b54c3863813d80a39eb4b2463f6d..a200bb881182fd2d521b7474ca318054b207f18a 100644
--- a/brian_polling_manager/configuration.py
+++ b/brian_polling_manager/configuration.py
@@ -29,14 +29,19 @@ _DEFAULT_CONFIG = {
         'interface-check': {
             'script': '/var/lib/sensu/bin/counter2influx.sh',
             'measurement': 'counters',
-            'interval': 300,
-            'subscriptions': ['interfacecounters'],
-            'output_metric_handlers': ['influx-db-handler'],
-            'namespace': 'default',
-            'round_robin': True,
             'command': ('{script} {measurement} '
                         '{community} {hostname} '
                         '{interface} {ifIndex}'),
+        },
+        'gws-direct-interface-check': {
+            'script': '/var/lib/sensu/bin/poll-gws-direct.sh',
+            'measurement': 'gwsd_counters',
+            'command': '{script} {measurement} {nren} {isp} {hostname} {tag}'
+        },
+        'dscp32-service-check': {
+            'script': '/var/lib/sensu/bin/poll-gws-indirect.sh',
+            'measurement': 'dscp32_counters',
+            'command': '{script} {measurement} {service}'
         }
     },
     'statedir': '/tmp/',
@@ -56,22 +61,9 @@ CONFIG_SCHEMA = {
             'properties': {
                 'script': {'type': 'string'},
                 'measurement': {'type': 'string'},
-                'interval': {'type': 'integer'},
-                'subscriptions': {
-                    'type': 'array',
-                    'items': {'type': 'string'}
-                },
-                'output_metric_handlers': {
-                    'type': 'array',
-                    'items': {'type': 'string'}
-                },
-                'namespace': {'type': 'string'},
-                'round_robin': {'type': 'boolean'},
                 'command': {'type': 'string'},
             },
-            'required': ['script', 'measurement', 'interval',
-                         'subscriptions', 'output_metric_handlers',
-                         'namespace', 'round_robin', 'command'],
+            'required': ['script', 'measurement', 'command'],
             'additionalProperties': False
         },
         'sensu': {
@@ -83,9 +75,16 @@ CONFIG_SCHEMA = {
                     'minItems': 1
                 },
                 'api-key': {'type': 'string'},
-                'interface-check': {'$ref': '#/definitions/influx-check'}
+                'interface-check': {'$ref': '#/definitions/influx-check'},
+                'gws-direct-interface-check':
+                    {'$ref': '#/definitions/influx-check'},
+                'dscp32-service-check': {'$ref': '#/definitions/influx-check'},
             },
-            'required': ['api-base', 'api-key', 'interface-check'],
+            'required': [
+                'api-base', 'api-key',
+                'interface-check',
+                'gws-direct-interface-check',
+                'dscp32-service-check'],
             'additionalProperties': False
         },
         'statsd': {
@@ -118,6 +117,8 @@ CONFIG_SCHEMA = {
 
 class State(object):
 
+    GWS_DIRECT = 'gws-direct.json'
+    GWS_INDIRECT = 'gws-indirect.json'
     INTERFACES = 'interfaces.json'
     STATE = 'state.json'
 
@@ -133,9 +134,11 @@ class State(object):
 
     def __init__(self, state_dir: str):
         assert os.path.isdir(state_dir)
-        self.filenames = {
+        self.cache_filenames = {
             'state': os.path.join(state_dir, State.STATE),
-            'cache': os.path.join(state_dir, State.INTERFACES)
+            'interfaces': os.path.join(state_dir, State.INTERFACES),
+            'gws-direct': os.path.join(state_dir, State.GWS_DIRECT),
+            'gws-indirect': os.path.join(state_dir, State.GWS_INDIRECT)
         }
 
     @staticmethod
@@ -150,38 +153,71 @@ class State(object):
                 f'unable to open state file {filename}')
             return None
 
+    @staticmethod
+    def _save_json(filename, new_data, schema):
+        try:
+            jsonschema.validate(new_data, schema)
+        except jsonschema.ValidationError:
+            logger.exception('invalid interface state data')
+            return
+
+        with open(filename, 'w') as f:
+            f.write(json.dumps(new_data))
+
     @property
     def last(self) -> int:
-        state = State._load_json(self.filenames['state'], State.STATE_SCHEMA)
+        state = State._load_json(
+            self.cache_filenames['state'], State.STATE_SCHEMA)
         return state['last'] if state else -1
 
     @last.setter
     def last(self, new_last: Union[float, None]):
         if not new_last or new_last < 0:
-            os.unlink(self.filenames['state'])
+            os.unlink(self.cache_filenames['state'])
         else:
-            state = {'last': new_last}
-            with open(self.filenames['state'], 'w') as f:
-                f.write(json.dumps(state))
+            State._save_json(
+                self.cache_filenames['state'],
+                {'last': new_last},
+                State.STATE_SCHEMA)
 
     @property
     def interfaces(self) -> list:
         return State._load_json(
-            self.filenames['cache'],
+            self.cache_filenames['interfaces'],
             inventory.INVENTORY_INTERFACES_SCHEMA)
 
     @interfaces.setter
     def interfaces(self, new_interfaces):
-        try:
-            jsonschema.validate(
-                new_interfaces,
-                inventory.INVENTORY_INTERFACES_SCHEMA)
-        except jsonschema.ValidationError:
-            logger.exception('invalid interface state data')
-            return
+        State._save_json(
+            self.cache_filenames['interfaces'],
+            new_interfaces,
+            inventory.INVENTORY_INTERFACES_SCHEMA)
+
+    @property
+    def gws_direct(self) -> list:
+        return State._load_json(
+            self.cache_filenames['gws-direct'],
+            inventory.GWS_DIRECT_SCHEMA)
+
+    @gws_direct.setter
+    def gws_direct(self, new_interfaces):
+        State._save_json(
+            self.cache_filenames['gws-direct'],
+            new_interfaces,
+            inventory.GWS_DIRECT_SCHEMA)
+
+    @property
+    def gws_indirect(self) -> list:
+        return State._load_json(
+            self.cache_filenames['gws-indirect'],
+            inventory.GWS_INDIRECT_SCHEMA)
 
-        with open(self.filenames['cache'], 'w') as f:
-            f.write(json.dumps(new_interfaces))
+    @gws_indirect.setter
+    def gws_indirect(self, new_services):
+        State._save_json(
+            self.cache_filenames['gws-indirect'],
+            new_services,
+            inventory.GWS_INDIRECT_SCHEMA)
 
 
 def _setup_logging(filename=None):
diff --git a/brian_polling_manager/gws_direct.py b/brian_polling_manager/gws_direct.py
new file mode 100644
index 0000000000000000000000000000000000000000..12a4f92350ccff5ec1b2f29e736388df2fa1e62f
--- /dev/null
+++ b/brian_polling_manager/gws_direct.py
@@ -0,0 +1,57 @@
+from brian_polling_manager import sensu
+
+
+def load_gws_direct_checks(sensu_params):
+    def _is_gws_direct_check(check):
+        name = check['metadata']['name']
+        return name.startswith('gwsd')
+    ifc_checks = filter(
+        _is_gws_direct_check, sensu.load_all_checks(sensu_params))
+    return {c['metadata']['name']: c for c in ifc_checks}
+
+
+class GwSDirectInterfaceCheck(sensu.AbstractCheck):
+
+    def __init__(self, ifc_check_params, interface):
+        super().__init__()
+        self.ifc_check_params = ifc_check_params
+        self.interface = interface
+
+    @sensu.AbstractCheck.name.getter
+    def name(self):
+        isp = self.interface['isp']
+        isp = isp.replace(' ', '_')
+        tag = self.interface['tag']
+        tag = tag.replace(' ', '_')
+        return f'gwsd-{self.interface["nren"]}-{isp}-{tag}'
+
+    @sensu.AbstractCheck.command.getter
+    def command(self):
+        isp = self.interface["isp"]
+        if ' ' in isp:
+            isp = f'"{self.interface["isp"]}"'
+
+        return self.ifc_check_params['command'].format(
+            script=self.ifc_check_params['script'],
+            measurement=self.ifc_check_params['measurement'],
+            hostname=self.interface['hostname'],
+            isp=isp,
+            nren=self.interface['nren'],
+            tag=self.interface['tag'])
+
+    @sensu.AbstractCheck.proxy_entity_name.getter
+    def proxy_entity_name(self):
+        return self.interface['hostname']
+
+
+def refresh(sensu_params, inventory_interfaces):
+
+    required_checks = [
+        GwSDirectInterfaceCheck(
+            sensu_params['gws-direct-interface-check'], ifc)
+        for ifc in inventory_interfaces]
+
+    return sensu.refresh(
+        sensu_params,
+        required_checks,
+        load_gws_direct_checks(sensu_params))
diff --git a/brian_polling_manager/gws_indirect.py b/brian_polling_manager/gws_indirect.py
new file mode 100644
index 0000000000000000000000000000000000000000..64d2a57e25aef29505532fe32671e37ad6f7c101
--- /dev/null
+++ b/brian_polling_manager/gws_indirect.py
@@ -0,0 +1,47 @@
+import re
+from brian_polling_manager import sensu
+
+
+def load_dscp32_checks(sensu_params):
+    def _is_dscp32_check(check):
+        name = check['metadata']['name']
+        return name.startswith('dscp32')
+    ifc_checks = filter(
+        _is_dscp32_check, sensu.load_all_checks(sensu_params))
+    return {c['metadata']['name']: c for c in ifc_checks}
+
+
+class DSCP32CountersCheck(sensu.AbstractCheck):
+
+    def __init__(self, sensu_check_params, service):
+        super().__init__()
+        self.sensu_check_params = sensu_check_params
+        self.service = service
+
+    @sensu.AbstractCheck.name.getter
+    def name(self):
+        name = re.sub(r'[\s_-]+', '_', self.service['name'])
+        return f'dscp32-{name}'
+
+    @sensu.AbstractCheck.command.getter
+    def command(self):
+        return self.sensu_check_params['command'].format(
+            script=self.sensu_check_params['script'],
+            measurement=self.sensu_check_params['measurement'],
+            service=self.service['id'])
+
+    @sensu.AbstractCheck.proxy_entity_name.getter
+    def proxy_entity_name(self):
+        return self.service['hostname']
+
+
+def refresh(sensu_params, services):
+
+    required_checks = [
+        DSCP32CountersCheck(sensu_params['dscp32-service-check'], s)
+        for s in services]
+
+    return sensu.refresh(
+        sensu_params,
+        required_checks,
+        load_dscp32_checks(sensu_params))
diff --git a/brian_polling_manager/interfaces.py b/brian_polling_manager/interfaces.py
index f824c0d69d30262a4d87501f5b6291213b817131..ab6a15e2f8f03984d7c0c4c58a2f0fc8c8c3eb3a 100644
--- a/brian_polling_manager/interfaces.py
+++ b/brian_polling_manager/interfaces.py
@@ -11,100 +11,47 @@ def load_ifc_checks(sensu_params):
         name = check['metadata']['name']
         # check-* is the old-style name (add to the returned
         # data so it can be deleted)
-        return re.match(r'^(check|ifc)-([^-]+\.geant\.net)-(.+)$', name)
+        return re.match(r'^(check|ifc)-[^-]+\.geant\.net-.+$', name)
     ifc_checks = filter(_is_ifc_check, sensu.load_all_checks(sensu_params))
     return {c['metadata']['name']: c for c in ifc_checks}
 
 
-def _check_name(interface):
-    ifc_name = interface['name'].replace('/', '-')
-    return f'ifc-{interface["router"]}-{ifc_name}'
+class InterfaceCheck(sensu.AbstractCheck):
 
+    def __init__(self, ifc_check_params, interface):
+        super().__init__()
+        self.ifc_check_params = ifc_check_params
+        self.interface = interface
 
-def _make_check(check_params, interface):
-    command = check_params['command'].format(
-        script=check_params['script'],
-        measurement=check_params['measurement'],
-        community='0pBiFbD',  # TODO: add this to /poller/interfaces response
-        hostname=interface['router'],
-        interface=interface['name'],
-        ifIndex=interface['snmp-index']
-    )
+    @sensu.AbstractCheck.name.getter
+    def name(self):
+        ifc_name = self.interface['name'].replace('/', '-')
+        return f'ifc-{self.interface["router"]}-{ifc_name}'
 
-    return {
-        'command': command,
-        'interval': check_params['interval'],
-        'subscriptions': sorted(check_params['subscriptions']),
-        'proxy_entity_name': interface['router'],
-        'round_robin': check_params['round_robin'],
-        'output_metric_format': 'influxdb_line',
-        'output_metric_handlers': sorted(
-            check_params['output_metric_handlers']),
-        'metadata': {
-            'name': _check_name(interface),
-            'namespace': check_params['namespace']
-        },
-        'publish': True
-    }
+    @sensu.AbstractCheck.command.getter
+    def command(self):
+        return self.ifc_check_params['command'].format(
+            script=self.ifc_check_params['script'],
+            measurement=self.ifc_check_params['measurement'],
+            # TODO: add community string to /poller/interfaces response
+            # (cf. POL1-339)
+            community='0pBiFbD',
+            hostname=self.interface['router'],
+            interface=self.interface['name'],
+            ifIndex=self.interface['snmp-index'])
 
+    @sensu.AbstractCheck.proxy_entity_name.getter
+    def proxy_entity_name(self):
+        return self.interface['router']
 
-def _checks_match(a, b) -> bool:
-    if a['publish'] != b['publish']:
-        return False
-    if a['command'] != b['command']:
-        return False
-    if a['interval'] != b['interval']:
-        return False
-    if a['proxy_entity_name'] != b['proxy_entity_name']:
-        return False
-    if a['round_robin'] != b['round_robin']:
-        return False
-    if a['output_metric_format'] != b['output_metric_format']:
-        return False
-    if sorted(a['subscriptions']) != sorted(b['subscriptions']):
-        return False
-    if sorted(a['output_metric_handlers']) \
-            != sorted(b['output_metric_handlers']):
-        return False
-    if a['metadata']['name'] != b['metadata']['name']:
-        return False
-    if a['metadata']['namespace'] != b['metadata']['namespace']:
-        return False
-    return True
 
+def refresh(sensu_params, inventory_interfaces):
 
-def refresh(sensu_params, state):
+    required_checks = [
+        InterfaceCheck(sensu_params['interface-check'], ifc)
+        for ifc in inventory_interfaces]
 
-    ifc_checks = load_ifc_checks(sensu_params)
-
-    created = 0
-    updated = 0
-    interfaces = 0
-    for interface in state.interfaces:
-
-        interfaces += 1
-
-        expected_check = _make_check(
-            sensu_params['interface-check'], interface)
-
-        expected_name = _check_name(interface)
-        if expected_name not in ifc_checks:
-            created += 1
-            sensu.create_check(sensu_params, expected_check)
-        elif not _checks_match(ifc_checks[expected_name], expected_check):
-            updated += 1
-            sensu.update_check(sensu_params, expected_check)
-
-    wanted_checks = {_check_name(ifc) for ifc in state.interfaces}
-    extra_checks = set(ifc_checks.keys()) - wanted_checks
-    for name in extra_checks:
-        sensu.delete_check(sensu_params, name)
-
-    # cf. main.REFRESH_RESULT_SCHEMA
-    return {
-        'checks': len(ifc_checks),
-        'input': interfaces,
-        'created': created,
-        'updated': updated,
-        'deleted': len(extra_checks)
-    }
+    return sensu.refresh(
+        sensu_params,
+        required_checks,
+        load_ifc_checks(sensu_params))
diff --git a/brian_polling_manager/inventory.py b/brian_polling_manager/inventory.py
index b5211049129076e924420c21d2a7fd4ede1e0eee..5d118036f2002699866d11427d5036795f802df2 100644
--- a/brian_polling_manager/inventory.py
+++ b/brian_polling_manager/inventory.py
@@ -6,7 +6,7 @@ import requests
 
 logger = logging.getLogger(__name__)
 
-# minimal inventory response schema
+# minimal inventory response schema for our purposes
 INVENTORY_VERSION_SCHEMA = {
     '$schema': 'http://json-schema.org/draft-07/schema#',
 
@@ -29,50 +29,23 @@ INVENTORY_VERSION_SCHEMA = {
     'additionalProperties': True
 }
 
+# minimal inventory response schema for our purposes
 INVENTORY_INTERFACES_SCHEMA = {
     '$schema': 'http://json-schema.org/draft-07/schema#',
 
     'definitions': {
-        'service': {
-            'type': 'object',
-            'properties': {
-                'id': {'type': 'integer'},
-                'name': {'type': 'string'},
-                'type': {'type': 'string'},
-                'status': {'type': 'string'},
-            },
-            'required': ['id', 'name', 'type', 'status'],
-            'additionalProperties': False
-        },
         'interface': {
             'type': 'object',
             'properties': {
                 'router': {'type': 'string'},
                 'name': {'type': 'string'},
-                'description': {'type': 'string'},
                 'snmp-index': {
                     'type': 'integer',
                     'minimum': 1
-                },
-                'bundle': {
-                    'type': 'array',
-                    'items': {'type': 'string'}
-                },
-                'bundle-parents': {
-                    'type': 'array',
-                    'items': {'type': 'string'}
-                },
-                'circuits': {
-                    'type': 'array',
-                    'items': {'$ref': '#/definitions/service'}
                 }
             },
-            'required': [
-                'router', 'name', 'description',
-                'snmp-index', 'bundle', 'bundle-parents',
-                'circuits'],
-            'additionalProperties': False
-        },
+            'required': ['router', 'name', 'snmp-index'],
+        }
     },
 
     'type': 'array',
@@ -80,33 +53,112 @@ INVENTORY_INTERFACES_SCHEMA = {
 }
 
 
-def _pick_one(things):
-    if not isinstance(things, (list, tuple, set)):
-        things = [things]
-    return random.choice(things)
+# minimal inventory response schema for our purposes
+GWS_DIRECT_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-07/schema#',
 
+    'definitions': {
+        'interface-counters': {
+            'type': 'object',
+            'properties': {
+                'nren': {'type': 'string'},
+                'isp': {
+                    'type': 'string',
+                    'enum': ['Cogent', 'Telia', 'CenturyLink']
+                },
+                'hostname': {'type': 'string'},
+                'tag': {'type': 'string'},
+            },
+            'required': ['nren', 'isp', 'hostname', 'tag']
+        }
+    },
 
-def load_interfaces(base_urls):
+    'type': 'array',
+    'items': {'$ref': '#/definitions/interface-counters'}
+}
+
+GWS_INDIRECT_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+
+    'definitions': {
+        'service': {
+            'type': 'object',
+            'properties': {
+                # minimal validation for our purposes
+                'name': {'type': 'string'},
+                'hostname': {'type': 'string'}
+            },
+            'required': ['name', 'hostname']
+        }
+    },
+
+    'type': 'array',
+    'items': {'$ref': '#/definitions/service'}
+}
+
+
+def _pick_one(haystack):
+    if not isinstance(haystack, (list, tuple, set)):
+        haystack = [haystack]
+    return random.choice(haystack)
+
+
+def _load_inventory_json(api_route, base_urls, schema):
     """
-    Load /poller/interfaces from inventory provider
-    and return a slightly reformatted dict.
+    Load & decode the specified inventory api data
 
+    :param api_route: the api-specific handler route
     :param base_urls: inventory provider base api url, or a list of them
-    :return: a dict like [<router>][<interface>] = inventory leaf data
+    :param schema: jsonschema to validate the response against
+    :return: the decoded json reponse
     """
     url = _pick_one(base_urls)
     logger.debug(f'using inventory base api url: {url}')
 
     rsp = requests.get(
-        f'{url}/poller/interfaces',
+        f'{url}/{api_route}',
         headers={'Accept': 'application/json'})
     rsp.raise_for_status()
 
     result = rsp.json()
-    jsonschema.validate(result, INVENTORY_INTERFACES_SCHEMA)
+    jsonschema.validate(result, schema)
     return result
 
 
+def load_interfaces(base_urls):
+    """
+    Load /poller/interfaces from inventory provider
+    and return a slightly reformatted dict.
+
+    :param base_urls: inventory provider base api url, or a list of them
+    :return: a list (INVENTORY_INTERFACES_SCHEMA)
+    """
+    return _load_inventory_json(
+        'poller/interfaces', base_urls, INVENTORY_INTERFACES_SCHEMA)
+
+
+def load_gws_direct_interfaces(base_urls):
+    """
+    Load /poller/gws/direct from inventory provider
+
+    :param base_urls: inventory provider base api url, or a list of them
+    :return: an interable of interface-specific check data
+    """
+    return _load_inventory_json(
+        'poller/gws/direct', base_urls, GWS_DIRECT_SCHEMA)
+
+
+def load_gws_indirect_services(base_urls):
+    """
+    Load /poller/gws/indirect from inventory provider
+
+    :param base_urls: inventory provider base api url, or a list of them
+    :return: an iterable of strings (service names)
+    """
+    return _load_inventory_json(
+        'poller/gws/indirect', base_urls, GWS_INDIRECT_SCHEMA)
+
+
 def last_update_timestamp(base_urls) -> float:
     try:
         r = requests.get(
diff --git a/brian_polling_manager/main.py b/brian_polling_manager/main.py
index 5e92a164b9706344ec9d558fd37ed2fd2fdc4473..ab837be04feb9235447bee664f800355493088b7 100644
--- a/brian_polling_manager/main.py
+++ b/brian_polling_manager/main.py
@@ -29,7 +29,8 @@ import click
 import jsonschema
 from statsd import StatsClient
 
-from brian_polling_manager import inventory, interfaces, configuration
+from brian_polling_manager \
+    import inventory, configuration, interfaces, gws_direct, gws_indirect
 
 logger = logging.getLogger(__name__)
 
@@ -51,7 +52,9 @@ REFRESH_RESULT_SCHEMA = {
     },
     'type': 'object',
     'properties': {
-        'interfaces': {'$ref': '#/definitions/refresh-result'}
+        'interfaces': {'$ref': '#/definitions/refresh-result'},
+        'gws_direct': {'$ref': '#/definitions/refresh-result'},
+        'gws_indirect': {'$ref': '#/definitions/refresh-result'}
     },
     'required': ['interfaces'],
     'additionalProperties': False
@@ -76,24 +79,31 @@ def refresh(config, force=False):
     if force or not last or last != state.last:
         state.last = last
         state.interfaces = inventory.load_interfaces(config['inventory'])
-
+        state.gws_direct = inventory.load_gws_direct_interfaces(
+            config['inventory'])
+        state.gws_indirect = inventory.load_gws_indirect_services(
+            config['inventory'])
     result = {
-        'interfaces': interfaces.refresh(config['sensu'], state)
+        'interfaces': interfaces.refresh(config['sensu'], state.interfaces),
+        'gws_direct': gws_direct.refresh(config['sensu'], state.gws_direct),
+        'gws_indirect': gws_indirect.refresh(
+            config['sensu'], state.gws_indirect),
     }
+    jsonschema.validate(result, REFRESH_RESULT_SCHEMA)  # sanity
 
     statsd_config = config.get('statsd', None)
     if statsd_config:
-        statsd = StatsClient(
-            host=statsd_config['hostname'],
-            port=statsd_config['port'],
-            prefix=f'{statsd_config["prefix"]}_interfaces')
-        statsd.gauge('checks', result['interfaces']['checks'])
-        statsd.gauge('input', result['interfaces']['input'])
-        statsd.gauge('created', result['interfaces']['created'])
-        statsd.gauge('updated', result['interfaces']['updated'])
-        statsd.gauge('deleted', result['interfaces']['deleted'])
+        for key, counts in result.items():
+            statsd = StatsClient(
+                host=statsd_config['hostname'],
+                port=statsd_config['port'],
+                prefix=f'{statsd_config["prefix"]}_{key}')
+            statsd.gauge('checks', counts['checks'])
+            statsd.gauge('input', counts['input'])
+            statsd.gauge('created', counts['created'])
+            statsd.gauge('updated', counts['updated'])
+            statsd.gauge('deleted', counts['deleted'])
 
-    jsonschema.validate(result, REFRESH_RESULT_SCHEMA)  # sanity
     return result
 
 
diff --git a/brian_polling_manager/sensu.py b/brian_polling_manager/sensu.py
index 0c60d091320f7ddfbc8a94aa5423b34c7a1dc9f2..b03ec13862c92fb7f1d8d0db4a5a0b3c094c939c 100644
--- a/brian_polling_manager/sensu.py
+++ b/brian_polling_manager/sensu.py
@@ -1,6 +1,7 @@
 """
 Sensu api utilities
 """
+import copy
 import logging
 import random
 import requests
@@ -8,7 +9,14 @@ import requests
 logger = logging.getLogger(__name__)
 
 
+_cached_checks = None  # not using lru_cache, since params is a dict
+
+
 def load_all_checks(params, namespace='default'):
+    global _cached_checks
+    if _cached_checks is not None:
+        return _cached_checks
+
     url = random.choice(params['api-base'])
     r = requests.get(
         f'{url}/api/core/v2/namespaces/{namespace}/checks',
@@ -18,8 +26,8 @@ def load_all_checks(params, namespace='default'):
         })
     r.raise_for_status()
 
-    for check in r.json():
-        yield check
+    _cached_checks = r.json()
+    return _cached_checks
 
 
 def create_check(params, check, namespace='default'):
@@ -60,3 +68,188 @@ def delete_check(params, check, namespace='default'):
         f'{url}/api/core/v2/namespaces/{namespace}/checks/{name}',
         headers={'Authorization': f'Key {params["api-key"]}'})
     r.raise_for_status()
+
+
+def checks_match(a, b) -> bool:
+    if a['publish'] != b['publish']:
+        return False
+    if a['command'] != b['command']:
+        return False
+    if a['interval'] != b['interval']:
+        return False
+    if a['proxy_entity_name'] != b['proxy_entity_name']:
+        return False
+    if a['round_robin'] != b['round_robin']:
+        return False
+    if a['output_metric_format'] != b['output_metric_format']:
+        return False
+    if sorted(a['subscriptions']) != sorted(b['subscriptions']):
+        return False
+    if sorted(a['output_metric_handlers']) \
+            != sorted(b['output_metric_handlers']):
+        return False
+    if a['metadata']['name'] != b['metadata']['name']:
+        return False
+    if a['metadata']['namespace'] != b['metadata']['namespace']:
+        return False
+    return True
+
+
+class AbstractCheck(object):
+    """
+    not explicitly using abc.ABC ... more readable than stacks of decorators
+    """
+
+    INTERVAL_S = 300
+    SUBSCRIPTIONS = ['interfacecounters']
+    METRIC_FORMAT = 'influxdb_line'
+    METRIC_HANDLERS = ['influx-db-handler']
+    NAMESPACE = 'default'
+
+    CHECK_DICT_SCHEMA = {
+        # for unit tests
+        '$schema': 'http://json-schema.org/draft-07/schema#',
+
+        'definitions': {
+            'metadata': {
+                'type': 'object',
+                'properties': {
+                    'name': {'type': 'string'},
+                    'namespace': {'type': 'string'}
+                },
+                'required': ['name', 'namespace']
+            }
+        },
+
+        'type': 'object',
+        'properties': {
+            'command': {'type': 'string'},
+            'interval': {'type': 'integer'},
+            'subscriptions': {
+                'type': 'array',
+                'items': {'type': 'string'},
+                'minItems': 1
+            },
+            'proxy_entity_name': {'type': 'string'},
+            'round_robin': {'type': 'boolean'},
+            'output_metric_format': {'type': 'string'},
+            'output_metric_handlers': {
+                'type': 'array',
+                'items': {'type': 'string'},
+                'minItems': 1
+            },
+            'metadata': {'$ref': '#/definitions/metadata'},
+            'publish': {'type': 'boolean'}
+        },
+        'required': [
+            'command', 'interval', 'subscriptions',
+            'proxy_entity_name', 'round_robin',
+            'output_metric_format', 'output_metric_handlers',
+            'metadata', 'publish']
+    }
+
+    def __init__(self):
+        self.publish = True
+        self.round_robin = True
+        self.interval = AbstractCheck.INTERVAL_S
+        self.subscriptions = copy.copy(AbstractCheck.SUBSCRIPTIONS)
+        self.output_metric_handlers = copy.copy(AbstractCheck.METRIC_HANDLERS)
+        self.output_metric_format = AbstractCheck.METRIC_FORMAT
+        self.namespace = AbstractCheck.NAMESPACE
+
+    @property
+    def name(self):
+        # overridden implementation must return a string
+        assert False, 'property StandardCheck.name must be overridden'
+
+    @property
+    def command(self):
+        # overridden implementation must return a string
+        assert False, 'property StandardCheck.command must be overridden'
+
+    @property
+    def proxy_entity_name(self):
+        # overridden implementation must return a string
+        assert False, \
+            'property StandardCheck.proxy_entity_name must be overridden'
+
+    def to_dict(self):
+        return {
+            'command': self.command,
+            'interval': self.interval,
+            'subscriptions': sorted(self.subscriptions),
+            'proxy_entity_name': self.proxy_entity_name,
+            'round_robin': self.round_robin,
+            'output_metric_format': self.output_metric_format,
+            'output_metric_handlers': sorted(self.output_metric_handlers),
+            'metadata': {
+                'name': self.name,
+                'namespace': self.namespace
+            },
+            'publish': self.publish
+        }
+
+    @staticmethod
+    def match_check_dicts(a, b) -> bool:
+        if a['publish'] != b['publish']:
+            return False
+        if a['command'] != b['command']:
+            return False
+        if a['interval'] != b['interval']:
+            return False
+        if a['proxy_entity_name'] != b['proxy_entity_name']:
+            return False
+        if a['round_robin'] != b['round_robin']:
+            return False
+        if a['output_metric_format'] != b['output_metric_format']:
+            return False
+        if sorted(a['subscriptions']) != sorted(b['subscriptions']):
+            return False
+        if sorted(a['output_metric_handlers']) \
+                != sorted(b['output_metric_handlers']):
+            return False
+        if a['metadata']['name'] != b['metadata']['name']:
+            return False
+        if a['metadata']['namespace'] != b['metadata']['namespace']:
+            return False
+        return True
+
+
+def refresh(sensu_params, required_checks, current_checks):
+    """
+    update any current_checks that are not present in required_checks
+    remove any extras
+
+    :param sensu_params:
+    :param required_checks: list of AbstractCheck instances
+    :param current_checks: dict of {name:check_dict} from sensu
+    :return: dict with change counts
+    """
+
+    # cf. main.REFRESH_RESULT_SCHEMA
+    result = {
+        'checks': len(current_checks),
+        'input': len(required_checks),
+        'created': 0,
+        'updated': 0,
+        'deleted': 0
+    }
+
+    for expected_check in required_checks:
+
+        if expected_check.name not in current_checks:
+            create_check(sensu_params, expected_check.to_dict())
+            result['created'] += 1
+        elif not AbstractCheck.match_check_dicts(
+                current_checks[expected_check.name],
+                expected_check.to_dict()):
+            update_check(sensu_params, expected_check.to_dict())
+            result['updated'] += 1
+
+    wanted_checks = {check.name for check in required_checks}
+    extra_checks = set(current_checks.keys()) - wanted_checks
+    for name in extra_checks:
+        delete_check(sensu_params, name)
+
+    result['deleted'] = len(extra_checks)
+    return result
diff --git a/setup.py b/setup.py
index 5869c7014767c852d01296b5faeb5a31625a889b..3a42b85f51d7e227bd1530c6d66791682acd8b4e 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
 
 setup(
     name='brian-polling-manager',
-    version="0.2",
+    version="0.3",
     author='GEANT',
     author_email='swd@geant.org',
     description='service for managing BRIAN polling checks',
diff --git a/test/conftest.py b/test/conftest.py
index 2553e8fcafecbeb13bf967ab4ae92e28ebe5f704..a2e99401dcc03df611c1d555c74562ffb496191b 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -36,14 +36,20 @@ def config():
                 'interface-check': {
                     'script': '/var/lib/sensu/bin/counter2influx.sh',
                     'measurement': 'counters',
-                    'interval': 300,
-                    'subscriptions': ['interfacecounters'],
-                    'output_metric_handlers': ['influx-db-handler'],
-                    'namespace': 'default',
-                    'round_robin': True,
                     'command': ('{script} {measurement} '
                                 '{community} {hostname} '
                                 '{interface} {ifIndex}'),
+                },
+                'gws-direct-interface-check': {
+                    'script': '/var/lib/sensu/bin/poll-gws-direct.sh',
+                    'measurement': 'gwsd_counters',
+                    'command': ('{script} {measurement} '
+                                '{nren} {isp} {hostname} {tag}')
+                },
+                'dscp32-service-check': {
+                    'script': '/var/lib/sensu/bin/poll-gws-indirect.sh',
+                    'measurement': 'dscp32_counters',
+                    'command': '{script} {measurement} {service}'
                 }
             },
             'statedir': state_dir_name,
@@ -163,12 +169,21 @@ def mocked_sensu():
 @pytest.fixture
 def mocked_inventory():
 
-    # mocked api for returning all checks
     responses.add(
         method=responses.GET,
         url=re.compile(r'.*inventory.+/poller/interfaces.*'),
         body=_load_test_data('interfaces.json'))
 
+    responses.add(
+        method=responses.GET,
+        url=re.compile(r'.*inventory.+/poller/gws/direct'),
+        body=_load_test_data('gws-direct.json'))
+
+    responses.add(
+        method=responses.GET,
+        url=re.compile(r'.*inventory.+/poller/gws/indirect.*'),
+        body=_load_test_data('gws-indirect.json'))
+
     bogus_version = {'latch': {'timestamp': 10000 * random.random()}}
     # mocked api for returning all checks
     responses.add(
diff --git a/test/data/checks.json b/test/data/checks.json
index 6ff1cee3be38169120c93b88181119e626d559e2..5e0232bb0c1d89a9c3a06518cd781c8069cb1f10 100644
--- a/test/data/checks.json
+++ b/test/data/checks.json
@@ -1,4 +1,95 @@
 [
+  {
+    "command": "/var/lib/sensu/bin/poll-gws-direct.sh gwsd_counters ARNES Cogent 88.200.0.63 a",
+    "handlers": [],
+    "high_flap_threshold": 0,
+    "interval": 300,
+    "low_flap_threshold": 0,
+    "publish": true,
+    "runtime_assets": null,
+    "subscriptions": [
+      "interfacecounters"
+    ],
+    "proxy_entity_name": "88.200.0.63",
+    "check_hooks": null,
+    "stdin": false,
+    "subdue": null,
+    "ttl": 0,
+    "timeout": 0,
+    "round_robin": true,
+    "output_metric_format": "influxdb_line",
+    "output_metric_handlers": [
+      "influx-db-handler"
+    ],
+    "env_vars": null,
+    "metadata": {
+      "name": "gwsd-06D560CF",
+      "namespace": "default",
+      "created_by": "admin"
+    },
+    "secrets": null
+  },
+  {
+    "command": "/var/lib/sensu/bin/poll-gws-direct.sh gwsd_counters ARNES Cogent 88.200.0.63 x-to-be-deleted",
+    "handlers": [],
+    "high_flap_threshold": 0,
+    "interval": 300,
+    "low_flap_threshold": 0,
+    "publish": true,
+    "runtime_assets": null,
+    "subscriptions": [
+      "interfacecounters"
+    ],
+    "proxy_entity_name": "88.200.0.63",
+    "check_hooks": null,
+    "stdin": false,
+    "subdue": null,
+    "ttl": 0,
+    "timeout": 0,
+    "round_robin": true,
+    "output_metric_format": "influxdb_line",
+    "output_metric_handlers": [
+      "influx-db-handler"
+    ],
+    "env_vars": null,
+    "metadata": {
+      "name": "gwsd-AAAAA",
+      "namespace": "default",
+      "created_by": "admin"
+    },
+    "secrets": null
+  },
+
+  {
+    "command": "/var/lib/sensu/bin/poll-gws-direct.sh gwsd_counters ARNES Cogent 88.200.0.63 to-be-updated",
+    "handlers": [],
+    "high_flap_threshold": 0,
+    "interval": 300,
+    "low_flap_threshold": 0,
+    "publish": true,
+    "runtime_assets": null,
+    "subscriptions": [
+      "interfacecounters"
+    ],
+    "proxy_entity_name": "88.200.0.63",
+    "check_hooks": null,
+    "stdin": false,
+    "subdue": null,
+    "ttl": 0,
+    "timeout": 0,
+    "round_robin": true,
+    "output_metric_format": "influxdb_line",
+    "output_metric_handlers": [
+      "influx-db-handler"
+    ],
+    "env_vars": null,
+    "metadata": {
+      "name": "gwsd-0B3898D4",
+      "namespace": "default",
+      "created_by": "admin"
+    },
+    "secrets": null
+  },
   {
     "command": "/var/lib/sensu/bin/counter2influx.sh counters 0pBiFbD mx1.ams.nl.geant.net ae1 1211",
     "handlers": [],
diff --git a/test/data/gws-direct.json b/test/data/gws-direct.json
new file mode 100644
index 0000000000000000000000000000000000000000..13708864c5b31be4b98630fcd174cea0172f3d1b
--- /dev/null
+++ b/test/data/gws-direct.json
@@ -0,0 +1,538 @@
+[
+  {
+    "nren": "ARNES",
+    "isp": "Cogent",
+    "hostname": "88.200.0.63",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "discards_in",
+        "oid": "1.3.6.1.2.1.2.2.1.13.533",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "discards_out",
+        "oid": "1.3.6.1.2.1.2.2.1.19.533",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "errors_in",
+        "oid": "1.3.6.1.2.1.2.2.1.14.533",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "errors_out",
+        "oid": "1.3.6.1.2.1.2.2.1.20.533",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "ARNES",
+    "isp": "Cogent",
+    "hostname": "88.200.0.63",
+    "tag": "b",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.531",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.531",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "ARNES",
+    "isp": "Cogent",
+    "hostname": "88.200.0.63",
+    "tag": "c",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.525",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.525",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "ARNES",
+    "isp": "Cogent",
+    "hostname": "88.200.0.63",
+    "tag": "d",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.553",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.553",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "ARNES",
+    "isp": "Cogent",
+    "hostname": "88.200.0.63",
+    "tag": "e",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.563",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.563",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "ARNES",
+    "isp": "Telia",
+    "hostname": "62.40.124.6",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.611",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.611",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "ARNES",
+    "isp": "Telia",
+    "hostname": "62.40.124.6",
+    "tag": "b",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.589",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.589",
+        "snmp": {
+          "community": "gn2nocT3st"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "CARNET",
+    "isp": "Cogent",
+    "hostname": "62.40.124.10",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.35",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.35",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "CARNET",
+    "isp": "Telia",
+    "hostname": "62.40.125.150",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.48",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.48",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "KIFU",
+    "isp": "Cogent",
+    "hostname": "195.111.97.108",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.155",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.155",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "KIFU",
+    "isp": "Telia",
+    "hostname": "195.111.97.108",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.148",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.148",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RedIRIS",
+    "isp": "Telia",
+    "hostname": "130.206.206.250",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.1487",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.1487",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RedIRIS",
+    "isp": "Telia",
+    "hostname": "130.206.206.250",
+    "tag": "b",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.1488",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.1488",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RedIRIS",
+    "isp": "Telia",
+    "hostname": "130.206.206.250",
+    "tag": "c",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.1489",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.1489",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RedIRIS",
+    "isp": "Telia",
+    "hostname": "130.206.206.250",
+    "tag": "d",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.760",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.760",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RedIRIS",
+    "isp": "Telia",
+    "hostname": "130.206.206.250",
+    "tag": "e",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.796",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.796",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RoEduNet",
+    "isp": "Cogent",
+    "hostname": "149.6.50.10",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.531",
+        "snmp": {
+          "community": "dante"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.531",
+        "snmp": {
+          "community": "dante"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "RoEduNet",
+    "isp": "CenturyLink",
+    "hostname": "212.162.45.194",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.9",
+        "snmp": {
+          "community": "dante"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.9",
+        "snmp": {
+          "community": "dante"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "EENet",
+    "isp": "Telia",
+    "hostname": "193.40.133.2",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.263",
+        "snmp": {
+          "community": "geant-mon-telia"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.263",
+        "snmp": {
+          "community": "geant-mon-telia"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "PSNC",
+    "isp": "CenturyLink",
+    "hostname": "212.191.126.6",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.675",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.675",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "PSNC",
+    "isp": "CenturyLink",
+    "hostname": "212.191.126.7",
+    "tag": "b",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.677",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.677",
+        "snmp": {
+          "community": "atlas1453"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "FCCN",
+    "isp": "Cogent",
+    "hostname": "193.136.5.43",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.47",
+        "snmp": {
+          "community": "geantcom"
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.47",
+        "snmp": {
+          "community": "geantcom"
+        }
+      }
+    ]
+  },
+  {
+    "nren": "HEANET",
+    "isp": "CenturyLink",
+    "hostname": "core2-cwt.nn.hea.net",
+    "tag": "a",
+    "counters": [
+      {
+        "field": "traffic_in",
+        "oid": "1.3.6.1.2.1.31.1.1.1.6.645",
+        "snmp": {
+          "sec-name": "geant",
+          "auth": {
+            "protocol": "MD5",
+            "password": "aagagag"
+          },
+          "priv": {
+            "protocol": "DES",
+            "password": "asdfadfads"
+          }
+        }
+      },
+      {
+        "field": "traffic_out",
+        "oid": "1.3.6.1.2.1.31.1.1.1.10.645",
+        "snmp": {
+          "sec-name": "geant",
+          "auth": {
+            "protocol": "MD5",
+            "password": "aagagag"
+          },
+          "priv": {
+            "protocol": "DES",
+            "password": "asdfadfads"
+          }
+        }
+      }
+    ]
+  }
+]
diff --git a/test/data/gws-indirect.json b/test/data/gws-indirect.json
new file mode 100644
index 0000000000000000000000000000000000000000..c3b2d352adae77f4cf4f2f50e6d4cc74b53fa845
--- /dev/null
+++ b/test/data/gws-indirect.json
@@ -0,0 +1 @@
+[{"id": 662955, "name": "RENAM-AP-IAS", "customer": "RENAM", "speed": 10737418240, "pop": "BUCHAREST", "hostname": "mx1.buc.ro.geant.net", "interface": "ae12.333", "type": "GWS - INDIRECT", "status": "non-monitored"}, {"id": 702139, "name": "FR_ARN_IAS", "customer": "ARN", "speed": 10737418240, "pop": "MARSEILLE", "hostname": "mx1.mar.fr.geant.net", "interface": "ae18.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663103, "name": "AMRES-AP-IAS", "customer": "AMRES", "speed": 21474836480, "pop": "BUDAPEST", "hostname": "mx1.bud.hu.geant.net", "interface": "ae16.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663198, "name": "CYNET-AP3-IAS", "customer": "CYNET", "speed": 10737418240, "pop": "ATHENS 2", "hostname": "mx1.ath2.gr.geant.net", "interface": "ae12.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663075, "name": "BELNET-AP3-IAS", "customer": "BELNET", "speed": 107374182400, "pop": "AMSTERDAM", "hostname": "mx1.ams.nl.geant.net", "interface": "ae13.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661220, "name": "UK_ASREN_IAS", "customer": "ASREN", "speed": 10737418240, "pop": "LONDON 2", "hostname": "mx1.lon2.uk.geant.net", "interface": "ae17.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661313, "name": "MARNET-AP2-IAS", "customer": "MARNET", "speed": 10737418240, "pop": "VIENNA", "hostname": "mx1.vie.at.geant.net", "interface": "ae17.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661510, "name": "UOM-AP1-IAS", "customer": "UOM", "speed": 10737418240, "pop": "MILAN 2 CALDERA", "hostname": "mx1.mil2.it.geant.net", "interface": "xe-11/0/0.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661936, "name": "KIFU-AP2-IAS", "customer": "KIFU", "speed": 42949672960, "pop": "VIENNA", "hostname": "mx1.vie.at.geant.net", "interface": "ae18.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663070, "name": "KIFU_AP1_IAS", "customer": "KIFU", "speed": 107374182400, "pop": "BUDAPEST", "hostname": "mx1.bud.hu.geant.net", "interface": "ae10.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661641, "name": "FCCN-AP1-IAS", "customer": "FCCN", "speed": 42949672960, "pop": "LISBON", "hostname": "mx2.lis.pt.geant.net", "interface": "ae10.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663220, "name": "RESTENA-AP1-IAS", "customer": "RESTENA", "speed": 107374182400, "pop": "FRANKFURT", "hostname": "mx1.fra.de.geant.net", "interface": "ae18.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661496, "name": "SANET_AP_IAS", "customer": "SANET", "speed": 21474836480, "pop": "BRATISLAVA", "hostname": "mx2.bra.sk.geant.net", "interface": "ae13.421", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 662908, "name": "MREN-AP-IAS", "customer": "MREN", "speed": 10737418240, "pop": "BUDAPEST", "hostname": "mx1.bud.hu.geant.net", "interface": "ae15.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661308, "name": "RASH-AP1-IAS", "customer": "RASH", "speed": 10737418240, "pop": "MILAN 2 CALDERA", "hostname": "mx1.mil2.it.geant.net", "interface": "ae13.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663228, "name": "IUCC-AP2-IAS", "customer": "IUCC", "speed": 32212254720, "pop": "FRANKFURT", "hostname": "mx1.fra.de.geant.net", "interface": "ae21.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 679570, "name": "ROEDUNET-AP2-IAS", "customer": "ROEDUNET", "speed": 107374182400, "pop": "VIENNA", "hostname": "mx1.vie.at.geant.net", "interface": "ae21.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 702074, "name": "CYNET_AP2_IAS", "customer": "CYNET", "speed": 10737418240, "pop": "FRANKFURT", "hostname": "mx1.fra.de.geant.net", "interface": "ae38.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 707416, "name": "LITNET-AP2-IAS", "customer": "LITNET", "speed": 10737418240, "pop": "KAUNAS", "hostname": "rt1.kau.lt.geant.net", "interface": "xe-0/1/1.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 662907, "name": "GRNET-AP2-IAS", "customer": "GRNET", "speed": 42949672960, "pop": "ATHENS 2", "hostname": "mx1.ath2.gr.geant.net", "interface": "ae11.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 673674, "name": "BELNET_AP2_IAS", "customer": "BELNET", "speed": 107374182400, "pop": "LONDON", "hostname": "mx1.lon.uk.geant.net", "interface": "ae16.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661222, "name": "FCCN-AP2-IAS", "customer": "FCCN", "speed": 42949672960, "pop": "LISBON 2", "hostname": "mx1.lis.pt.geant.net", "interface": "ae10.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661977, "name": "GRNET-AP1-IAS", "customer": "GRNET", "speed": 42949672960, "pop": "ATHENS", "hostname": "mx2.ath.gr.geant.net", "interface": "ae10.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661500, "name": "IUCC-AP1-IAS", "customer": "IUCC", "speed": 32212254720, "pop": "LONDON", "hostname": "mx1.lon.uk.geant.net", "interface": "ae21.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 663112, "name": "ROEDUNET_AP1_IAS", "customer": "ROEDUNET", "speed": 42949672960, "pop": "BUCHAREST", "hostname": "mx1.buc.ro.geant.net", "interface": "ae11.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 678593, "name": "URAN-AP1-IAS", "customer": "URAN", "speed": 21474836480, "pop": "VIENNA", "hostname": "mx1.vie.at.geant.net", "interface": "ae20.333", "type": "GWS - INDIRECT", "status": "operational"}, {"id": 661703, "name": "LITNET_AP1_IAS", "customer": "LITNET", "speed": 10737418240, "pop": "KAUNAS", "hostname": "rt2.kau.lt.geant.net", "interface": "xe-0/1/1.333", "type": "GWS - INDIRECT", "status": "operational"}]
\ No newline at end of file
diff --git a/test/test_sensu_checks.py b/test/test_sensu_checks.py
index 7a5a8178cfd72ff4d3d6a1899b1d1012e45f5df4..b72dd0578a020747bf5a648f1933a5c2a5cf6148 100644
--- a/test/test_sensu_checks.py
+++ b/test/test_sensu_checks.py
@@ -1,6 +1,7 @@
 import copy
 import random
 
+import jsonschema
 import responses
 
 from brian_polling_manager import sensu, inventory, interfaces
@@ -8,7 +9,7 @@ from brian_polling_manager import sensu, inventory, interfaces
 
 @responses.activate
 def test_load_checks(config, mocked_sensu):
-    checks = list(sensu.load_all_checks(config['sensu']))
+    checks = sensu.load_all_checks(config['sensu'])
     assert len(checks) > 0  # test data has checks in it
 
 
@@ -19,9 +20,9 @@ def test_check_lifecycle(config, mocked_sensu, mocked_inventory):
         inventory.load_interfaces(config['inventory']))
     test_interface['name'] = 'xyz'
 
-    new_check = interfaces._make_check(
+    new_check = interfaces.InterfaceCheck(
         config['sensu']['interface-check'],
-        test_interface)
+        test_interface).to_dict()
 
     # create the new check
     check_name = new_check['metadata']['name']
@@ -38,3 +39,84 @@ def test_check_lifecycle(config, mocked_sensu, mocked_inventory):
     # delete the check and confirm the correct call was made
     sensu.delete_check(config['sensu'], check_name)
     assert check_name not in mocked_sensu
+
+
+class DummyCheck(sensu.AbstractCheck):
+
+    def __init__(self, name, command, proxy_entity_name):
+        super().__init__()
+        self._name = name
+        self._command = command
+        self._proxy_entity_name = proxy_entity_name
+
+    @sensu.AbstractCheck.name.getter
+    def name(self):
+        return self._name
+
+    @sensu.AbstractCheck.command.getter
+    def command(self):
+        return self._command
+
+    @sensu.AbstractCheck.proxy_entity_name.getter
+    def proxy_entity_name(self):
+        return self._proxy_entity_name
+
+
+def test_check_dict_schema():
+    c = DummyCheck(name='x', command='y', proxy_entity_name='z')
+    jsonschema.validate(c.to_dict(), sensu.AbstractCheck.CHECK_DICT_SCHEMA)
+
+
+def test_check_compare():
+    a = DummyCheck(name='x', command='y', proxy_entity_name='z')
+    b = DummyCheck(name='x', command='y', proxy_entity_name='z')
+    assert sensu.checks_match(a.to_dict(), b.to_dict())
+
+
+def test_checks_differ():
+    a = DummyCheck(name='x', command='x', proxy_entity_name='1')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='2')
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='1', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='2', proxy_entity_name='x')
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='1', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='2', command='x', proxy_entity_name='x')
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.publish = not a.publish
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.interval += 1
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.round_robin = not a.round_robin
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.output_metric_format = a.output_metric_format + 'x'
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.subscriptions.append('x')
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.output_metric_handlers.append('x')
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())
+
+    a = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    b = DummyCheck(name='x', command='x', proxy_entity_name='x')
+    a.namespace = a.namespace + 'x'
+    assert not sensu.checks_match(a.to_dict(), b.to_dict())