App-scoped telemetry and usage metering¶
Two narrower reads over the telemetry a device owner already holds, for the two marketplace roles that are neither the owner nor an operator:
a publisher supporting its own app on someone else’s robots reads only the records that app produced;
a license authority reads derived usage counters for the robots it licenses, and never the readings behind them.
Both are ordinary grants on the /access/v1/grants surface, and both are read on
the /telemetry/v1 capability group. Neither needs a separate credential, endpoint
host, or authentication scheme — see Registration.
The app namespace¶
A telemetry record carries an app_namespace: which app on the device produced the
sample. It is a slug — ^[a-z][a-z0-9]*(-[a-z0-9]+)*$, 3 to 63 characters — and it
is the same slug the publisher declares for its app at publish time, so one
vocabulary spans the app, the grant, and the query.
{ "time": "2026-08-19T04:00:00Z",
"thing_id": "ROBOT-LAWSON-001",
"account_id": "acc-owner-01",
"scope_id": "scp-store-robots",
"app_namespace": "clearjanitor",
"payload": { "battery_pct": 82 } }
The field is absent on a record whose device did not report one. Absent is a
real, permanent state — it is what every record from a device that does not tag its
telemetry looks like — and it never matches an app_namespace filter. An
app-scoped reader therefore does not inherit a robot’s untagged history.
Warning
The app namespace is device-asserted. Nothing on the device plane attests which application spoke: every app on a robot shares the device’s one identity. The namespace narrows a read the owner has already granted, and is never on its own the reason a read is allowed — the trusted axes are the device identity and the owner and scope the platform resolves from it, server-side. Do not build a security control on the namespace: a compromised app on that robot can mis-tag its samples, and the bound on the damage is that the owner opted in per publisher and can revoke.
Reading one app’s telemetry¶
GET /telemetry/v1?scope_id=scp-store-robots&app_namespace=clearjanitor&from=…&to=…
Parameter |
Notes |
|---|---|
|
required when |
|
the app slug; narrows the read to that app’s records |
|
optional; one device |
|
RFC 3339; default the last 24 hours; maximum span 30 days |
|
default 1000, maximum 10000 |
Adding app_namespace changes which resource the read is authorized against.
The whole-scope read and the app-scoped read are two disjoint resources, not a
filter applied after a single decision. Three consequences you have to design
around:
An app-scoped grantee must always send
app_namespace. The same request without it is judged against the whole-scope resource, which an app-scoped grant does not cover, and is refused with403. This is the mechanism, not an oversight: it is a resource that does not match, rather than a rule that has to be remembered.app_namespacewithoutscope_idis400, not403. A grant hangs off a scope, so an app-only resource is one no grant can ever produce. Serving it would return an authorization-shaped answer to a malformed request and send you hunting for a grant that was never the problem. Treat the400as “fix the request”; treat a403as “the grant does not cover this”.A namespace containing
*,:,/or whitespace is400. It is rejected before any authorization decision is made.
The grant an owner writes for a publisher¶
POST /access/v1/grants
{
"grantee_account_id": "acc-publisher-01",
"scope_id": "scp-store-robots",
"caps": ["telemetry:read"],
"app_namespace": "clearjanitor"
}
Rule |
Behaviour |
|---|---|
|
anything else with |
|
otherwise |
|
updating a grant’s caps cannot widen an app-scoped grant off its app; revoke and re-create instead |
uniqueness is |
one publisher may hold two grants over one scope, one per app; a repeat of the same triple is |
Inside a grant, the app-scoped authorization replaces the whole-scope one rather than adding to it — naming an app never leaves the grantee holding the whole scope as well. Revoking the grant removes the app-scoped access with it.
The usage-metering read¶
GET /telemetry/v1/usage?scope_id=scp-store-robots&bucket=day&from=…&to=…
Authorized by its own capability, telemetry:usage-read, granted the same way:
POST /access/v1/grants
{ "grantee_account_id": "acc-licensor-01",
"scope_id": "scp-store-robots",
"caps": ["telemetry:usage-read"] }
Parameter |
Notes |
|---|---|
|
required |
|
|
|
optional; one device |
|
RFC 3339; default the last 30 days; maximum span 92 days |
|
optional cap on the number of returned buckets |
{ "bucket": "day",
"from": "2026-07-20T00:00:00Z",
"to": "2026-08-19T00:00:00Z",
"scope_id": "scp-store-robots",
"items": [
{ "bucket_start": "2026-08-18T00:00:00Z",
"bucket_end": "2026-08-19T00:00:00Z",
"thing_id": "ROBOT-LAWSON-001",
"sample_count": 1440,
"active_minutes": 372 }
] }
sample_count— how many records the robot produced in the bucket. A volume signal, not a duration.active_minutes— how many distinct minutes the robot reported in at all. Reporting cadence is a device and app decision, so this is counted rather than derived fromsample_countand an assumed rate: a chattier robot is not a harder-working one.
Two properties worth designing against:
There is no payload on this feed, ever. telemetry:usage-read is a distinct
capability over a distinct resource, not a restricted mode of telemetry:read. A
principal holding only it is refused /telemetry/v1 outright — the guarantee is an
authorization fact, not a promise about which fields a response happens to carry.
A window that does not fit is refused, not truncated. If the request would
produce more buckets than the maximum, the response is 400 telling you to narrow
from/to, widen bucket, or filter by thing_id. You are metering from these
numbers; a silently short page would under-report usage and look exactly like a
quiet fleet.
From the CLI¶
# one app's telemetry on a shared scope
fgai telemetry list --scope-id scp-store-robots --app-namespace clearjanitor
# per-robot usage counters, daily
fgai telemetry usage-list --scope-id scp-store-robots --bucket day
# the owner side: grant, list, revoke
fgai access grants-create --body '{"grantee_account_id":"acc-publisher-01","scope_id":"scp-store-robots","caps":["telemetry:read"],"app_namespace":"clearjanitor"}'
fgai access grants-list
fgai access grants-delete --id <grant-id>
See also¶
Thing access model — scopes, grants, and the full capability set.
Identity, grants, and application access — how a decision is made.
Device attributes — the config surface, whose write authority is single-writer.