This document describes a minimal implementation of a federated marketplace based on ActivityPub protocol and Valueflows vocabulary. In such marketplace actors can publish offers and requests, respond to offers and requests published by other actors, enter into agreements and exchange information necessary to complete these agreements.
Extension of ActivityPub protocol with Valueflows vocabulary was initially proposed by Lynn Foster in FEP-d767.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC-2119.
sequenceDiagram
actor Alice
actor Bob
Note right of Alice: Alice publishes a Proposal
Bob ->> Alice: Bob sends Offer(Agreement) activity
Alice ->> Bob: Alice sends Accept(Agreement) activity
Note over Alice, Bob: Alice and Bob complete the transaction
Alice ->> Bob: Alice sends confirmation activity
Valueflows defines proposals as published requests or offers, sometimes with what is expected in return.
The representation of a proposal is a JSON document with the following properties:
id
(REQUIRED): the proposal’s unique global identifier.type
(REQUIRED): the type of the object SHOULD be Proposal
. If interoperability with other ActivityPub services is desirable, implementers MAY use object types from Activity Vocabulary, such as Note
.purpose
(REQUIRED): the type of proposal. Possible values are offer
and request
.attributedTo
(REQUIRED): the actor who published the proposal.name
(RECOMMENDED): the title of the proposal.content
(OPTIONAL): the description of the proposal. The type of content SHOULD be text/html
.published
(RECOMMENDED): the date and time at which the proposal was published.location
(OPTIONAL): indicates a physical location associated with the proposal. The representation of location MUST conform to the recommendations of Activity Vocabulary document, section 5.3 Representing Places.publishes
(REQUIRED): the primary intent of this proposal (see below).reciprocal
(OPTIONAL): the reciprocal intent of this proposal (see below).unitBased
(OPTIONAL): indicates whether the quantities expressed in the proposal can be multiplied or not.to
(REQUIRED): the audience of the proposal.Intents are proposed economic transactions. The primary intent describes what is being offered or requested, and reciprocal intent describes what is expected or offered in return. Some examples:
The representation of an intent is a JSON document with the following properties:
id
(REQUIRED): the unique global identifier of the intent. Implementations SHOULD use URI fragments to identify intents associated with a given proposal. The RECOMMENDED fragment identifiers for primary and reciprocal intents are primary
and reciprocal
.type
(REQUIRED): the type of the object MUST be Intent
.action
(REQUIRED): the type of economic transaction. The value of this property SHOULD be either deliverService
or transfer
.resourceConformsTo
(RECOMMENDED): the type of an economic resource. Could be any URI.resourceQuantity
(REQUIRED): the amount and unit of the economic resource. This is an object with two properties:
hasUnit
(REQUIRED): name of the unit, according to Ontology of units of Measure classification. The RECOMMENDED unit for countable items is one
.hasNumericalValue
(OPTIONAL): the amount of the resource. If not specified, arbitrary amounts can be used when responding to the proposal.availableQuantity
(OPTIONAL): the quantity of the offered resource currently available.Minimal example:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"om2": "http://www.ontology-of-units-of-measure.org/resource/om-2/",
"vf": "https://w3id.org/valueflows/ont/vf#",
"Proposal": "vf:Proposal",
"Intent": "vf:Intent",
"action": "vf:action",
"purpose": "vf:purpose",
"unitBased": "vf:unitBased",
"publishes": "vf:publishes",
"resourceQuantity": "vf:resourceQuantity",
"hasUnit": "om2:hasUnit",
"hasNumericalValue": "om2:hasNumericalValue"
}
],
"type": "Proposal",
"id": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930",
"purpose": "offer",
"attributedTo": "https://market.example/users/alice",
"publishes": {
"type": "Intent",
"id": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#primary",
"action": "transfer",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "1"
}
},
"unitBased": false,
"to": "https://www.w3.org/ns/activitystreams#Public"
}
Full example:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"om2": "http://www.ontology-of-units-of-measure.org/resource/om-2/",
"vf": "https://w3id.org/valueflows/ont/vf#",
"Proposal": "vf:Proposal",
"Intent": "vf:Intent",
"action": "vf:action",
"purpose": "vf:purpose",
"unitBased": "vf:unitBased",
"publishes": "vf:publishes",
"reciprocal": "vf:reciprocal",
"resourceConformsTo": "vf:resourceConformsTo",
"resourceQuantity": "vf:resourceQuantity",
"availableQuantity": "vf:availableQuantity",
"hasUnit": "om2:hasUnit",
"hasNumericalValue": "om2:hasNumericalValue"
}
],
"type": "Proposal",
"id": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930",
"purpose": "offer",
"attributedTo": "https://market.example/users/alice",
"name": "Offering used bike",
"content": "Blue one-speed bike, 15 years old, some rust",
"published": "2023-06-18T19:22:03.918737Z",
"location": {
"type": "Place",
"longitude": -71.0,
"latitude": 25.0
},
"publishes": {
"type": "Intent",
"id": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#primary",
"action": "transfer",
"resourceConformsTo": "https://www.wikidata.org/wiki/Q11442",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "1"
},
"availableQuantity": {
"hasUnit": "one",
"hasNumericalValue": "1"
}
},
"reciprocal": {
"type": "Intent",
"id": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#reciprocal",
"action": "transfer",
"resourceConformsTo": "https://www.wikidata.org/wiki/Q4917",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "30"
}
},
"unitBased": false,
"to": "https://www.w3.org/ns/activitystreams#Public"
}
Proposals can be linked to actors (if actor provides a service) or to other objects (if they represent economic resources) using FEP-0ea0 payment links. Proposals can also be added to public collections, or be delivered to actor’s followers using Create
activity, or announced by group actors.
If FEP-0ea0 payment link is used, its href
attribute MUST contain the proposal ID and its rel
array MUST contain the string https://w3id.org/valueflows/ont/vf#Proposal
. The value of mediaType
attribute SHOULD be application/ld+json; profile="https://www.w3.org/ns/activitystreams"
.
Example of a proposal attached to an actor via payment link:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Person",
"id": "https://market.example/users/alice",
"inbox": "https://market.example/users/alice",
"outbox": "https://market.example/users/alice",
"attachment": [
{
"type": "Link",
"name": "Buy a bike",
"mediaType": "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930",
"rel": ["payment", "https://w3id.org/valueflows/ont/vf#Proposal"]
}
]
}
Consuming implementations which don’t have marketplace features MAY display proposals similarly to Note
objects.
An interested party responds to a proposal and then parties start negotiating to reach an agreement.
To respond to a proposal, an interested party MUST send an Agreement
object wrapped in Offer
activity to the actor indicated by the attributedTo
property of the proposal. The proposing party MUST either commit to the action described in the proposal or send a rejection.
In the first case, the proposer finalizes the agreement and sends Accept(Offer)
activity back to the interested party.
In the second case, the proposer sends Reject(Offer)
activity. The interested party MAY send Offer(Agreement)
activities many times until agreement is reached.
The representation of an agreement is a JSON document with the following properties:
id
(OPTIONAL): the unique global identifier of the agreement. This property is REQUIRED for finalized agreements.type
(REQUIRED): the type of the object MUST be Agreement
.stipulates
(REQUIRED): the primary commitment associated with the agreement.stipulatesReciprocal
(OPTIONAL): the reciprocal commitment associated with the agreement. It is REQUIRED if the corresponding proposal has a reciprocal intent.Commitments are promised economic transactions. The representation of a commitment is a JSON document with the following properties:
id
(OPTIONAL): the unique global identifier of the commitment. This property is REQUIRED for commitments in finalized agreements. Implementations SHOULD use URI fragments to identify commitments associated with a given agreement. The RECOMMENDED fragment identifiers for commitments satisfying primary and reciprocal intents of the proposal are primary
and reciprocal
.type
(REQUIRED): the type of the object MUST be Commitment
.satisfies
(REQUIRED): the reference to an intent.resourceQuantity
(REQUIRED): the amount and unit of the economic resource.The first commitment MUST satisfy the primary intent of the proposal. The second commitment MUST satisfy the reciprocal intent of the proposal (if present).
The units specified in the agreement MUST match the units specified in the proposal. If the value of unitBased
property of the proposal is false
, the amount of resources specified in commitments MUST be equal to amounts specified in the proposal. If the value is true
, amounts MUST be multiples of amounts specified in the proposal. If unitBased
property is not present on the proposal, arbitrary amounts can be used.
Example of an Offer(Agreement)
activity:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"om2": "http://www.ontology-of-units-of-measure.org/resource/om-2/",
"vf": "https://w3id.org/valueflows/ont/vf#",
"Agreement": "vf:Agreement",
"stipulates": "vf:stipulates",
"stipulatesReciprocal": "vf:stipulatesReciprocal",
"Commitment": "vf:Commitment",
"satisfies": "vf:satisfies",
"resourceQuantity": "vf:resourceQuantity",
"hasUnit": "om2:hasUnit",
"hasNumericalValue": "om2:hasNumericalValue"
}
],
"type": "Offer",
"id": "https://social.example/objects/fc4af0d2-c3a1-409b-947c-3c5be29f49b0/offer",
"actor": "https://social.example/users/bob",
"object": {
"type": "Agreement",
"stipulates": {
"type": "Commitment",
"satisfies": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#primary",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "1"
}
},
"stipulatesReciprocal": {
"type": "Commitment",
"satisfies": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#reciprocal",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "30"
}
}
},
"to": "https://market.example/users/alice"
}
The object
of Accept
activity MUST be the id
of the Offer
activity previously sent to the actor.
Accept
activity MUST have the result
property containing the Agreement
object. The finalized agreement and corresponding commitments MUST have an id
property. If a similar agreement between parties already exists, it MAY be updated and its id
re-used. The quantities specified in the finalized agreement MUST match the quantities specified in Agreement
object from the Offer
activity.
The finalized agreement MAY have url
property containing one or more links to resources associated with the agreement. An example of such resource is a payment page (which can be represented as an FEP-0ea0 link).
Example:
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"om2": "http://www.ontology-of-units-of-measure.org/resource/om-2/",
"vf": "https://w3id.org/valueflows/ont/vf#",
"Agreement": "vf:Agreement",
"stiplulates": "vf:stiplulates",
"stipulatesReciprocal": "vf:stipulatesReciprocal",
"Commitment": "vf:Commitment",
"satisfies": "vf:satisfies",
"resourceQuantity": "vf:resourceQuantity",
"hasUnit": "om2:hasUnit",
"hasNumericalValue": "om2:hasNumericalValue"
}
],
"type": "Accept",
"id": "https://market.example/activities/059f08fa-31b1-4136-8d76-5987d705a0ab",
"actor": "https://market.example/users/alice",
"object": "https://social.example/objects/fc4af0d2-c3a1-409b-947c-3c5be29f49b0/offer",
"result": {
"type": "Agreement",
"id": "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2",
"attributedTo": "https://market.example/users/alice",
"stiplulates": {
"id": "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#primary",
"type": "Commitment",
"satisfies": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#primary",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "1"
}
},
"stipulatesReciprocal": {
"id": "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2#reciprocal",
"type": "Commitment",
"satisfies": "https://market.example/proposals/ddde9d6f-6f3b-4770-a966-3a18ef006930#reciprocal",
"resourceQuantity": {
"hasUnit": "one",
"hasNumericalValue": "30"
}
},
"url": {
"type": "Link",
"href": "https://pay.example/invoices/7f1f0c81-0108-4c91-9cb1-d38ebccc3aa1",
"rel": "payment"
}
},
"to": "https://social.example/users/bob"
}
The object
of Reject
activity MUST be the id
of the Offer
activity previously sent to the actor.
Activity MAY contain content
property indicating the reason for rejection.
Example:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Reject",
"id": "https://market.example/activities/8c05f97f-1531-4b70-9ca8-4ee4a09f36a4",
"actor": "https://market.example/users/alice",
"object": "https://social.example/objects/fc4af0d2-c3a1-409b-947c-3c5be29f49b0/offer",
"content": "Not available",
"to": "https://social.example/users/bob"
}
Economic transaction happens outside the protocol. When both parties complete their parts of the transaction, the proposing party MUST publish a confirmation.
The type and structure of confirmation activity may vary between different marketplaces, but it MUST contain a reference to the Agreement
object. The context
property is RECOMMENDED for this purpose.
Example:
{
"@context": [
"https://www.w3.org/ns/activitystreams"
],
"type": "Create",
"id": "https://market.example/receipts/ad2f7ee1-6567-413e-a10b-72650cbdc743/create",
"actor": "https://market.example/users/alice",
"object": {
"type": "Document",
"id": "https://market.example/receipts/ad2f7ee1-6567-413e-a10b-72650cbdc743",
"name": "Receipt",
"context": "https://market.example/agreements/edc374aa-e580-4a58-9404-f3e8bf8556b2",
"published": "2023-07-03T14:13:41.843794Z"
},
"to": "https://social.example/users/bob"
}
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
To the extent possible under law, the authors of this Fediverse Enhancement Proposal have waived all copyright and related or neighboring rights to this work.