GitHub

Docs

PATH CONNECT

Bounded, revocable authorisation — for delegated applications, recurring mandates and agents acting on someone's behalf.

LIVE for PATH-CONNECT.Core and PATH-CONNECT.Delegation — connection objects, binds_to tokens, counters, usage log and revocation.

A connection is not a payment request. It names a grantor, a grantee and a subject. It is not served at /path/{reference}.

The problem it solves

Granting an application permission to move money is usually all or nothing. The application asks for access, the user says yes, and from then on nobody — including the user — can answer three basic questions:

  • What exactly is it allowed to do?
  • How much of that has it already used?
  • How do I stop it, right now, without closing the account?

PATH CONNECT makes the grant an object with a scope, a counter and a revocation.

The connection

{
  "type": "path.connection",
  "reference": "9kq3m7vx2pf8dn",
  "grantor": "member-a",
  "grantee": "member-b",
  "subject": "9f2c41a8-…",
  "capabilities": {
    "payment_request": true,
    "view_balance": false
  },
  "policy": {
    "max_single": "100000",
    "max_total": "500000",
    "currency": "USD",
    "expires_at": "2027-01-01T00:00:00Z"
  },
  "status": "active",
  "consumed": { "total": "125000", "operations": 7 },
  "remaining": { "total": "375000" },
  "consent": { "…": "the envelope signed when the subject agreed" },
  "consent_digest": "8f29…",
  "kid": "op_example_2026_01",
  "signature": "…"
}

Directional: this party is authorised against that account, for these capabilities, within these limits. Not "connected", which says nothing about direction or extent.

A capability that is absent is denied. There is no permissive default, so a typo in capabilities grants nothing rather than everything.

Two envelopes, and why

The outer signature is produced at read, because status and the counters move — a grant read today has been used more than it was yesterday. consent is the envelope signed when the subject agreed, stored byte for byte and never rebuilt.

Verify terms against consent, never against the fields beside it. Rebuilding them from the database would fail: numeric(38, 18) returns "100000.000000000000000000" for terms consented to as "100000" — same amount, different bytes, different canonical JSON, and a consent that no longer verifies. Receipts hit this first.

Connections are read at GET /connect/connections/{reference}, authenticated, and only by the two parties. They are not served behind /path/{reference}.

A payment request is meant to be scanned by a payer nobody knows. A connection names a grantor, a grantee and a subject, and carries limits and counters — serving it to whoever holds the reference would publish a private arrangement.

Two modes

ModeEach operationSuits
per_operation_signatureSigned individually by the subjectHigh value, low frequency
persistent_consentCovered by the standing grant, within limitsLow value, high frequency

persistent_consent is where the limits earn their place. Without a ceiling and a counter it is just an unbounded grant with better paperwork.

Delegation

LIVE

A delegation token authorises a third party — an application, an agent — to act within a subset of a connection. The grantee already consumes via POST /connect/connections/{reference}/consume. The token is for someone who is neither grantor nor grantee. Giving them the grantee's credential would hand them the whole grant.

Only the grantor issues (POST /connect/connections/{reference}/delegations). The parent grantee can still revoke the token — it is their grant being sliced.

Two properties are non-negotiable.

It is bound to the operation

{
  "binds_to": {
    "capability": "payment_request",
    "amount": "5000",
    "currency": "USD",
    "destination_commitment": "8f29a1c0…"
  }
}

A token that authorises "a payment" authorises every payment. Binding it to the amount and to a commitment over the destination means an intercepted token cannot be redirected or resized. Without the binding, a token stolen in transit is a blank cheque with an expiry date.

All four fields are required. Consume demands an exact match: a smaller amount is a different operation, not a safer one. max_total defaults to the bound amount (one use); set it higher for N identical operations — same amount, same destination — not a different one.

Not served behind /path/{reference}, for the same reason as a connection.

Its counters are visible

{
  "limits": { "max_total": "500000", "currency": "USD" },
  "consumed": { "total": "125000", "operations": 7 },
  "remaining": { "total": "375000" }
}

A limit nobody can read is a limit nobody can plan against. The holder of a token should be able to see what remains without attempting an operation and being refused — and a user reviewing their grants should see the same numbers.

remaining is computed at read and never stored. Two columns that must agree eventually disagree, and the one nobody writes to is the one that goes stale.

Counters are incremented under a lock

POST /connect/connections/{reference}/consume checks the grantee, the status, the expiry, the capability, the currency and both ceilings, then increments the counters and appends to the usage log — all inside one statement holding a row lock.

Reading a counter and then incrementing it lets two concurrent operations clear the same ceiling. That is a nuisance on a request status and a hole on a spending limit.

Mandates

A recurring mandate is a connection with a schedule. This is where subscribe links land, and the reason they belong here rather than in PATH REQUEST:

A payment request is a claim — one amount, one deadline, gone once paid. A mandate is a permission — it persists, it recurs, and it must be revocable.

Treat a subscription as a payment request and you ship without revocation and without counters, and discover it when a customer asks how to cancel.

The mandate digest — the canonical form of the terms a subject consented to — is computed over canonical JSON. Anything else and two implementations produce different digests for identical terms, which makes the consent unverifiable across a boundary.

Revocation

Immediate, unilateral, and available to the subject at any time.

Three things a specification should require and implementations often miss:

It takes effect at execution, not at issuance. A token issued this morning and revoked since must fail now. Any cached authorisation status is a hole. Here the grantee cannot spend anything without calling the grantor, who reads the state on every operation — so there is no cache to outlive the revocation.

It is broadcast. A revoked delegation must stop being honoured across the network, not only at the operator that revoked it.

Revocations of a connection are not written to the network's revocation register, and that is deliberate. Telling the directory that member A granted member B access to one of its subjects is a ring-3 fact the network has no business holding. The requirement is that no honouring party keeps a stale authorisation, and checking at execution satisfies it without publishing the grant.

It leaves the record intact. Revocation ends the permission; it does not erase what was done under it. A dispute needs the history — GET /connect/connections/{reference}/usage keeps it, and the log survives revocation.

Either party may end it. The grantor withdraws, the grantee renounces, and revoked_by records which — the two are not the same statement in a dispute.

Financial connection requests

A connection request that asks for financial capability follows the same object with an explicit consent step, and one rule about ordering that is easy to get backwards:

Match before consent. The subject is shown who is asking and what for before being asked to approve — and a request that does not match a known counterparty never reaches a consent screen at all. A consent screen shown for an unresolved counterparty trains people to approve things they cannot evaluate, which is the mechanism behind most authorised-push-payment fraud.


On this page